Merge "Drop ranking update events when another one is already queued."
diff --git a/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java b/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java
index 973e996..762b16c 100644
--- a/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java
+++ b/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java
@@ -24,19 +24,17 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.perftests.utils.BenchmarkState;
 import android.perftests.utils.PerfStatusReporter;
-
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
-
+import java.io.File;
+import java.util.Random;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.Random;
-
 /**
  * Performance tests for typical CRUD operations and loading rows into the Cursor
  *
@@ -58,12 +56,9 @@
     public void setUp() {
         mContext = InstrumentationRegistry.getTargetContext();
         mContext.deleteDatabase(DB_NAME);
-        mDatabase = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
-        mDatabase.execSQL("CREATE TABLE T1 "
-                + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)");
-        mDatabase.execSQL("CREATE TABLE T2 ("
-                + "_ID INTEGER PRIMARY KEY, COL_A VARCHAR(100), T1_ID INTEGER,"
-                + "FOREIGN KEY(T1_ID) REFERENCES T1 (_ID))");
+
+        createOrOpenTestDatabase(
+                SQLiteDatabase.JOURNAL_MODE_TRUNCATE, SQLiteDatabase.SYNC_MODE_FULL);
     }
 
     @After
@@ -72,6 +67,25 @@
         mContext.deleteDatabase(DB_NAME);
     }
 
+    private void createOrOpenTestDatabase(String journalMode, String syncMode) {
+        SQLiteDatabase.OpenParams.Builder paramsBuilder = new SQLiteDatabase.OpenParams.Builder();
+        File dbFile = mContext.getDatabasePath(DB_NAME);
+        if (journalMode != null) {
+            paramsBuilder.setJournalMode(journalMode);
+        }
+        if (syncMode != null) {
+            paramsBuilder.setSynchronousMode(syncMode);
+        }
+        paramsBuilder.addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY);
+
+        mDatabase = SQLiteDatabase.openDatabase(dbFile, paramsBuilder.build());
+        mDatabase.execSQL("CREATE TABLE T1 "
+                + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)");
+        mDatabase.execSQL("CREATE TABLE T2 ("
+                + "_ID INTEGER PRIMARY KEY, COL_A VARCHAR(100), T1_ID INTEGER,"
+                + "FOREIGN KEY(T1_ID) REFERENCES T1 (_ID))");
+    }
+
     @Test
     public void testSelect() {
         insertT1TestDataSet();
@@ -192,22 +206,114 @@
         }
     }
 
+    /**
+     * This test measures the insertion of a single row into a database using DELETE journal and
+     * synchronous modes.
+     */
     @Test
     public void testInsert() {
         insertT1TestDataSet();
 
+        testInsertInternal("testInsert");
+    }
+
+    @Test
+    public void testInsertWithPersistFull() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL);
+        insertT1TestDataSet();
+        testInsertInternal("testInsertWithPersistFull");
+    }
+
+    private void testInsertInternal(String traceTag) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
 
         ContentValues cv = new ContentValues();
         cv.put("_ID", DEFAULT_DATASET_SIZE);
         cv.put("COL_B", "NewValue");
         cv.put("COL_C", 1.1);
-        String[] deleteArgs = new String[]{String.valueOf(DEFAULT_DATASET_SIZE)};
+        String[] deleteArgs = new String[] {String.valueOf(DEFAULT_DATASET_SIZE)};
+
         while (state.keepRunning()) {
+            android.os.Trace.beginSection(traceTag);
             assertEquals(DEFAULT_DATASET_SIZE, mDatabase.insert("T1", null, cv));
             state.pauseTiming();
             assertEquals(1, mDatabase.delete("T1", "_ID=?", deleteArgs));
             state.resumeTiming();
+            android.os.Trace.endSection();
+        }
+    }
+
+    /**
+     * This test measures the insertion of a single row into a database using WAL journal mode and
+     * NORMAL synchronous mode.
+     */
+    @Test
+    public void testInsertWithWalNormalMode() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
+        insertT1TestDataSet();
+
+        testInsertInternal("testInsertWithWalNormalMode");
+    }
+
+    /**
+     * This test measures the insertion of a single row into a database using WAL journal mode and
+     * FULL synchronous mode. The goal is to see the difference between NORMAL vs FULL sync modes.
+     */
+    @Test
+    public void testInsertWithWalFullMode() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_FULL);
+
+        insertT1TestDataSet();
+
+        testInsertInternal("testInsertWithWalFullMode");
+    }
+
+    /**
+     * This test measures the insertion of a multiple rows in a single transaction using WAL journal
+     * mode and NORMAL synchronous mode.
+     */
+    @Test
+    public void testBulkInsertWithWalNormalMode() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
+        testBulkInsertInternal("testBulkInsertWithWalNormalMode");
+    }
+
+    @Test
+    public void testBulkInsertWithPersistFull() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL);
+        testBulkInsertInternal("testBulkInsertWithPersistFull");
+    }
+
+    /**
+     * This test measures the insertion of a multiple rows in a single transaction using TRUNCATE
+     * journal mode and FULL synchronous mode.
+     */
+    @Test
+    public void testBulkInsert() {
+        testBulkInsertInternal("testBulkInsert");
+    }
+
+    private void testBulkInsertInternal(String traceTag) {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        String[] statements = new String[DEFAULT_DATASET_SIZE];
+        for (int i = 0; i < DEFAULT_DATASET_SIZE; ++i) {
+            statements[i] = "INSERT INTO T1 VALUES (?,?,?,?)";
+        }
+
+        while (state.keepRunning()) {
+            android.os.Trace.beginSection(traceTag);
+            mDatabase.beginTransaction();
+            for (int i = 0; i < DEFAULT_DATASET_SIZE; ++i) {
+                mDatabase.execSQL(statements[i], new Object[] {i, i, "T1Value" + i, i * 1.1});
+            }
+            mDatabase.setTransactionSuccessful();
+            mDatabase.endTransaction();
+            android.os.Trace.endSection();
+
+            state.pauseTiming();
+            mDatabase.execSQL("DELETE FROM T1");
+            state.resumeTiming();
         }
     }
 
@@ -227,9 +333,30 @@
         }
     }
 
+    /**
+     * This test measures the update of a random row in a database.
+     */
+    @Test
+    public void testUpdateWithWalNormalMode() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
+        insertT1TestDataSet();
+        testUpdateInternal("testUpdateWithWalNormalMode");
+    }
+
+    @Test
+    public void testUpdateWithPersistFull() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL);
+        insertT1TestDataSet();
+        testUpdateInternal("testUpdateWithPersistFull");
+    }
+
     @Test
     public void testUpdate() {
         insertT1TestDataSet();
+        testUpdateInternal("testUpdate");
+    }
+
+    private void testUpdateInternal(String traceTag) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
 
         Random rnd = new Random(0);
@@ -237,6 +364,7 @@
         ContentValues cv = new ContentValues();
         String[] argArray = new String[1];
         while (state.keepRunning()) {
+            android.os.Trace.beginSection(traceTag);
             int id = rnd.nextInt(DEFAULT_DATASET_SIZE);
             cv.put("COL_A", i);
             cv.put("COL_B", "UpdatedValue");
@@ -244,6 +372,109 @@
             argArray[0] = String.valueOf(id);
             assertEquals(1, mDatabase.update("T1", cv, "_ID=?", argArray));
             i++;
+            android.os.Trace.endSection();
+        }
+    }
+
+    /**
+     * This test measures a multi-threaded read-write environment where there are 2 readers and
+     * 1 writer in the database using TRUNCATE journal mode and FULL syncMode.
+     */
+    @Test
+    public void testMultithreadedReadWrite() {
+        insertT1TestDataSet();
+        performMultithreadedReadWriteTest();
+    }
+
+    private void doReadLoop(int totalIterations) {
+        Random rnd = new Random(0);
+        int currentIteration = 0;
+        while (currentIteration < totalIterations) {
+            android.os.Trace.beginSection("ReadDatabase");
+            int index = rnd.nextInt(DEFAULT_DATASET_SIZE);
+            try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 "
+                                 + "WHERE _ID=?",
+                         new String[] {String.valueOf(index)})) {
+                cursor.moveToNext();
+                cursor.getInt(0);
+                cursor.getInt(1);
+                cursor.getString(2);
+                cursor.getDouble(3);
+            }
+            ++currentIteration;
+            android.os.Trace.endSection();
+        }
+    }
+
+    private void doReadLoop(BenchmarkState state) {
+        Random rnd = new Random(0);
+        while (state.keepRunning()) {
+            android.os.Trace.beginSection("ReadDatabase");
+            int index = rnd.nextInt(DEFAULT_DATASET_SIZE);
+            try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 "
+                                 + "WHERE _ID=?",
+                         new String[] {String.valueOf(index)})) {
+                cursor.moveToNext();
+                cursor.getInt(0);
+                cursor.getInt(1);
+                cursor.getString(2);
+                cursor.getDouble(3);
+            }
+            android.os.Trace.endSection();
+        }
+    }
+
+    private void doUpdateLoop(int totalIterations) {
+        SQLiteDatabase db = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
+        Random rnd = new Random(0);
+        int i = 0;
+        ContentValues cv = new ContentValues();
+        String[] argArray = new String[1];
+
+        while (i < totalIterations) {
+            android.os.Trace.beginSection("UpdateDatabase");
+            int id = rnd.nextInt(DEFAULT_DATASET_SIZE);
+            cv.put("COL_A", i);
+            cv.put("COL_B", "UpdatedValue");
+            cv.put("COL_C", i);
+            argArray[0] = String.valueOf(id);
+            db.update("T1", cv, "_ID=?", argArray);
+            i++;
+            android.os.Trace.endSection();
+        }
+    }
+
+    /**
+     * This test measures a multi-threaded read-write environment where there are 2 readers and
+     * 1 writer in the database using WAL journal mode and NORMAL syncMode.
+     */
+    @Test
+    public void testMultithreadedReadWriteWithWalNormal() {
+        recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
+        insertT1TestDataSet();
+
+        performMultithreadedReadWriteTest();
+    }
+
+    private void performMultithreadedReadWriteTest() {
+        int totalBGIterations = 10000;
+        // Writer - Fixed iterations to avoid consuming cycles from mainloop benchmark iterations
+        Thread updateThread = new Thread(() -> { doUpdateLoop(totalBGIterations); });
+
+        // Reader 1 - Fixed iterations to avoid consuming cycles from mainloop benchmark iterations
+        Thread readerThread = new Thread(() -> { doReadLoop(totalBGIterations); });
+
+        updateThread.start();
+        readerThread.start();
+
+        // Reader 2
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        doReadLoop(state);
+
+        try {
+            updateThread.join();
+            readerThread.join();
+        } catch (Exception e) {
         }
     }
 
@@ -270,5 +501,11 @@
         mDatabase.setTransactionSuccessful();
         mDatabase.endTransaction();
     }
+
+    private void recreateTestDatabase(String journalMode, String syncMode) {
+        mDatabase.close();
+        mContext.deleteDatabase(DB_NAME);
+        createOrOpenTestDatabase(journalMode, syncMode);
+    }
 }
 
diff --git a/core/api/current.txt b/core/api/current.txt
index 86f03eb..5f7e330 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -14376,11 +14376,21 @@
     field public static final int CONFLICT_ROLLBACK = 1; // 0x1
     field public static final int CREATE_IF_NECESSARY = 268435456; // 0x10000000
     field public static final int ENABLE_WRITE_AHEAD_LOGGING = 536870912; // 0x20000000
+    field public static final String JOURNAL_MODE_DELETE = "DELETE";
+    field public static final String JOURNAL_MODE_MEMORY = "MEMORY";
+    field public static final String JOURNAL_MODE_OFF = "OFF";
+    field public static final String JOURNAL_MODE_PERSIST = "PERSIST";
+    field public static final String JOURNAL_MODE_TRUNCATE = "TRUNCATE";
+    field public static final String JOURNAL_MODE_WAL = "WAL";
     field public static final int MAX_SQL_CACHE_SIZE = 100; // 0x64
     field public static final int NO_LOCALIZED_COLLATORS = 16; // 0x10
     field public static final int OPEN_READONLY = 1; // 0x1
     field public static final int OPEN_READWRITE = 0; // 0x0
     field public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000; // 0xc350
+    field public static final String SYNC_MODE_EXTRA = "EXTRA";
+    field public static final String SYNC_MODE_FULL = "FULL";
+    field public static final String SYNC_MODE_NORMAL = "NORMAL";
+    field public static final String SYNC_MODE_OFF = "OFF";
   }
 
   public static interface SQLiteDatabase.CursorFactory {
@@ -42980,8 +42990,8 @@
     method @NonNull public java.util.List<android.net.Uri> getDeviceToDeviceStatusSharingContacts(int);
     method public int getDeviceToDeviceStatusSharingPreference(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getOpportunisticSubscriptions();
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, "android.permission.READ_PRIVILEGED_PHONE_STATE"}) public String getPhoneNumber(int, int);
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, "android.permission.READ_PRIVILEGED_PHONE_STATE"}) public String getPhoneNumber(int);
+    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public String getPhoneNumber(int, int);
+    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public String getPhoneNumber(int);
     method public static int getSlotIndex(int);
     method @Nullable public int[] getSubscriptionIds(int);
     method @NonNull public java.util.List<android.telephony.SubscriptionPlan> getSubscriptionPlans(int);
@@ -42993,7 +43003,7 @@
     method public void removeOnOpportunisticSubscriptionsChangedListener(@NonNull android.telephony.SubscriptionManager.OnOpportunisticSubscriptionsChangedListener);
     method public void removeOnSubscriptionsChangedListener(android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void removeSubscriptionsFromGroup(@NonNull java.util.List<java.lang.Integer>, @NonNull android.os.ParcelUuid);
-    method public void setCarrierPhoneNumber(int, @NonNull String);
+    method @RequiresPermission("carrier privileges") public void setCarrierPhoneNumber(int, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDeviceToDeviceStatusSharingContacts(int, @NonNull java.util.List<android.net.Uri>);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDeviceToDeviceStatusSharingPreference(int, int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setOpportunistic(boolean, int);
@@ -45808,8 +45818,10 @@
 
   public class StyleSpan extends android.text.style.MetricAffectingSpan implements android.text.ParcelableSpan {
     ctor public StyleSpan(int);
+    ctor public StyleSpan(int, int);
     ctor public StyleSpan(@NonNull android.os.Parcel);
     method public int describeContents();
+    method public int getFontWeightAdjustment();
     method public int getSpanTypeId();
     method public int getStyle();
     method public void updateDrawState(android.text.TextPaint);
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 0bbc80c..b8ce02e7 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -79,7 +79,7 @@
   }
 
   public abstract class ContentResolver {
-    method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public final void registerContentObserverAsUser(@NonNull android.net.Uri, boolean, @NonNull android.database.ContentObserver, @NonNull android.os.UserHandle);
+    method @RequiresPermission(value=android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional=true) public final void registerContentObserverAsUser(@NonNull android.net.Uri, boolean, @NonNull android.database.ContentObserver, @NonNull android.os.UserHandle);
   }
 
   public abstract class Context {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index a298354..992789b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -5403,6 +5403,7 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForCapturePresetChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener) throws java.lang.SecurityException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener) throws java.lang.SecurityException;
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void cancelMuteAwaitConnection(@NonNull android.media.AudioDeviceAttributes) throws java.lang.IllegalStateException;
     method public void clearAudioServerStateCallback();
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean clearPreferredDevicesForCapturePreset(int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int dispatchAudioFocusChange(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy);
@@ -5416,6 +5417,7 @@
     method @IntRange(from=0) public long getMaxAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo);
     method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMaxVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
     method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMinVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
+    method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getMutingExpectedDevice();
     method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getPreferredDeviceForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForCapturePreset(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
@@ -5424,7 +5426,9 @@
     method public boolean isAudioServerRunning();
     method public boolean isHdmiSystemAudioSupported();
     method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable();
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
     method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForCapturePresetChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener);
@@ -5444,6 +5448,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setVolumeIndexForAttributes(@NonNull android.media.AudioAttributes, int, int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
     method public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback);
     field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1
     field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4
@@ -5463,6 +5468,15 @@
     method public void onAudioServerUp();
   }
 
+  public abstract static class AudioManager.MuteAwaitConnectionCallback {
+    ctor public AudioManager.MuteAwaitConnectionCallback();
+    method public void onMutedUntilConnection(@NonNull android.media.AudioDeviceAttributes, @NonNull int[]);
+    method public void onUnmutedEvent(int, @NonNull android.media.AudioDeviceAttributes, @NonNull int[]);
+    field public static final int EVENT_CANCEL = 3; // 0x3
+    field public static final int EVENT_CONNECTION = 1; // 0x1
+    field public static final int EVENT_TIMEOUT = 2; // 0x2
+  }
+
   @Deprecated public static interface AudioManager.OnPreferredDeviceForStrategyChangedListener {
     method @Deprecated public void onPreferredDeviceForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @Nullable android.media.AudioDeviceAttributes);
   }
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1014673..fee4ed8 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1458,6 +1458,7 @@
     method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable();
     method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int requestAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String, int, int);
     method public void setRampingRingerEnabled(boolean);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setTestDeviceConnectionState(@NonNull android.media.AudioDeviceAttributes, boolean);
   }
 
   public static final class AudioRecord.MetricsConstants {
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 48ceef0..2392c9a 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -626,6 +626,9 @@
      * foreground ({@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_FOREGROUND}
      * and the {@link android.service.quicksettings.TileService} must be exported.
      *
+     * Note: the system can choose to auto-deny a request if the user has denied that specific
+     * request (user, ComponentName) enough times before.
+     *
      * @param tileServiceComponentName {@link ComponentName} of the
      *        {@link android.service.quicksettings.TileService} for the request.
      * @param tileLabel label of the tile to show to the user.
diff --git a/core/java/android/companion/OWNERS b/core/java/android/companion/OWNERS
index 54b35fc..004f66c 100644
--- a/core/java/android/companion/OWNERS
+++ b/core/java/android/companion/OWNERS
@@ -1,4 +1,5 @@
 ewol@google.com
 evanxinchen@google.com
 guojing@google.com
-svetoslavganov@google.com
\ No newline at end of file
+svetoslavganov@google.com
+sergeynv@google.com
\ No newline at end of file
diff --git a/core/java/android/content/AttributionSource.aidl b/core/java/android/content/AttributionSource.aidl
index 10d5c27..7554cb2 100644
--- a/core/java/android/content/AttributionSource.aidl
+++ b/core/java/android/content/AttributionSource.aidl
@@ -16,4 +16,5 @@
 
 package android.content;
 
+@JavaOnlyStableParcelable
 parcelable AttributionSource;
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index adae599..184acb1 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -2684,7 +2684,8 @@
      * @hide
      * @see #unregisterContentObserver
      */
-    @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+    @RequiresPermission(value = android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+            conditional = true)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public final void registerContentObserverAsUser(@NonNull Uri uri,
             boolean notifyForDescendants,
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 58f20ef..45d6ddd 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1017,12 +1017,12 @@
      * This change id restricts treatments that force a given min aspect ratio to activities
      * whose orientation is fixed to portrait.
      *
-     * This treatment only takes effect if OVERRIDE_MIN_ASPECT_RATIO is also enabled.
+     * This treatment is enabled by default and only takes effect if OVERRIDE_MIN_ASPECT_RATIO is
+     * also enabled.
      * @hide
      */
     @ChangeId
     @Overridable
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S_V2)
     @TestApi
     public static final long OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY = 203647190L; // buganizer id
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 1911670..3bf5f31 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -3651,6 +3651,14 @@
     public static final String FEATURE_MANAGED_PROFILES = "android.software.managed_users";
 
     /**
+     * @hide
+     * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
+     * The device supports Nearby mainline feature.
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_NEARBY = "android.software.nearby";
+
+    /**
      * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
      * The device supports verified boot.
      */
diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java
index 5bc235f..6c07356 100644
--- a/core/java/android/content/res/StringBlock.java
+++ b/core/java/android/content/res/StringBlock.java
@@ -16,8 +16,9 @@
 
 package android.content.res;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.app.Application;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.graphics.Color;
 import android.graphics.Paint;
@@ -238,7 +239,10 @@
 
 
             if (type == ids.boldId) {
-                buffer.setSpan(new StyleSpan(Typeface.BOLD),
+                Application application = ActivityThread.currentApplication();
+                int fontWeightAdjustment =
+                        application.getResources().getConfiguration().fontWeightAdjustment;
+                buffer.setSpan(new StyleSpan(Typeface.BOLD, fontWeightAdjustment),
                                style[i+1], style[i+2]+1,
                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
             } else if (type == ids.italicId) {
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
index 1db948a..ccb7cf1 100644
--- a/core/java/android/database/CursorWindow.java
+++ b/core/java/android/database/CursorWindow.java
@@ -143,7 +143,6 @@
             throw new AssertionError(); // Not possible, the native code won't return it.
         }
         mCloseGuard.open("close");
-        recordNewWindow(Binder.getCallingPid(), mWindowPtr);
     }
 
     /**
@@ -191,7 +190,6 @@
             mCloseGuard.close();
         }
         if (mWindowPtr != 0) {
-            recordClosingOfWindow(mWindowPtr);
             nativeDispose(mWindowPtr);
             mWindowPtr = 0;
         }
@@ -746,64 +744,6 @@
         dispose();
     }
 
-    @UnsupportedAppUsage
-    private static final LongSparseArray<Integer> sWindowToPidMap = new LongSparseArray<Integer>();
-
-    private void recordNewWindow(int pid, long window) {
-        synchronized (sWindowToPidMap) {
-            sWindowToPidMap.put(window, pid);
-            if (Log.isLoggable(STATS_TAG, Log.VERBOSE)) {
-                Log.i(STATS_TAG, "Created a new Cursor. " + printStats());
-            }
-        }
-    }
-
-    private void recordClosingOfWindow(long window) {
-        synchronized (sWindowToPidMap) {
-            if (sWindowToPidMap.size() == 0) {
-                // this means we are not in the ContentProvider.
-                return;
-            }
-            sWindowToPidMap.delete(window);
-        }
-    }
-
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    private String printStats() {
-        StringBuilder buff = new StringBuilder();
-        int myPid = Process.myPid();
-        int total = 0;
-        SparseIntArray pidCounts = new SparseIntArray();
-        synchronized (sWindowToPidMap) {
-            int size = sWindowToPidMap.size();
-            if (size == 0) {
-                // this means we are not in the ContentProvider.
-                return "";
-            }
-            for (int indx = 0; indx < size; indx++) {
-                int pid = sWindowToPidMap.valueAt(indx);
-                int value = pidCounts.get(pid);
-                pidCounts.put(pid, ++value);
-            }
-        }
-        int numPids = pidCounts.size();
-        for (int i = 0; i < numPids;i++) {
-            buff.append(" (# cursors opened by ");
-            int pid = pidCounts.keyAt(i);
-            if (pid == myPid) {
-                buff.append("this proc=");
-            } else {
-                buff.append("pid ").append(pid).append('=');
-            }
-            int num = pidCounts.get(pid);
-            buff.append(num).append(')');
-            total += num;
-        }
-        // limit the returned string size to 1000
-        String s = (buff.length() > 980) ? buff.substring(0, 980) : buff.toString();
-        return "# Open Cursors=" + total + s;
-    }
-
     private static int getCursorWindowSize() {
         if (sCursorWindowSize < 0) {
             // The cursor window size. resource xml file specifies the value in kB.
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index 7e61b48..328858b 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -26,14 +26,13 @@
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.LruCache;
 import android.util.Pair;
 import android.util.Printer;
-
 import dalvik.system.BlockGuard;
 import dalvik.system.CloseGuard;
-
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.FileSystems;
@@ -177,7 +176,7 @@
         mConfiguration = new SQLiteDatabaseConfiguration(configuration);
         mConnectionId = connectionId;
         mIsPrimaryConnection = primaryConnection;
-        mIsReadOnlyConnection = (configuration.openFlags & SQLiteDatabase.OPEN_READONLY) != 0;
+        mIsReadOnlyConnection = mConfiguration.isReadOnlyDatabase();
         mPreparedStatementCache = new PreparedStatementCache(
                 mConfiguration.maxSqlCacheSize);
         mCloseGuard.open("close");
@@ -266,7 +265,8 @@
         }
         setPageSize();
         setForeignKeyModeFromConfiguration();
-        setWalModeFromConfiguration();
+        setJournalFromConfiguration();
+        setSyncModeFromConfiguration();
         setJournalSizeLimit();
         setAutoCheckpointInterval();
         setLocaleFromConfiguration();
@@ -334,30 +334,19 @@
         }
     }
 
-    private void setWalModeFromConfiguration() {
-        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
-            final boolean walEnabled =
-                    (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0;
-            // Use compatibility WAL unless an app explicitly set journal/synchronous mode
-            // or DISABLE_COMPATIBILITY_WAL flag is set
-            final boolean isCompatibilityWalEnabled =
-                    mConfiguration.isLegacyCompatibilityWalEnabled();
-            if (walEnabled || isCompatibilityWalEnabled) {
-                setJournalMode("WAL");
-                if (mConfiguration.syncMode != null) {
-                    setSyncMode(mConfiguration.syncMode);
-                } else if (isCompatibilityWalEnabled) {
-                    setSyncMode(SQLiteCompatibilityWalFlags.getWALSyncMode());
-                } else {
-                    setSyncMode(SQLiteGlobal.getWALSyncMode());
-                }
-                maybeTruncateWalFile();
-            } else {
-                setJournalMode(mConfiguration.journalMode == null
-                        ? SQLiteGlobal.getDefaultJournalMode() : mConfiguration.journalMode);
-                setSyncMode(mConfiguration.syncMode == null
-                        ? SQLiteGlobal.getDefaultSyncMode() : mConfiguration.syncMode);
-            }
+    private void setJournalFromConfiguration() {
+        if (!mIsReadOnlyConnection) {
+            setJournalMode(mConfiguration.resolveJournalMode());
+            maybeTruncateWalFile();
+        } else {
+            // No need to truncate for read only databases.
+            mConfiguration.shouldTruncateWalFile = false;
+        }
+    }
+
+    private void setSyncModeFromConfiguration() {
+        if (!mIsReadOnlyConnection) {
+            setSyncMode(mConfiguration.resolveSyncMode());
         }
     }
 
@@ -366,6 +355,10 @@
      * PRAGMA wal_checkpoint.
      */
     private void maybeTruncateWalFile() {
+        if (!mConfiguration.shouldTruncateWalFile) {
+            return;
+        }
+
         final long threshold = SQLiteGlobal.getWALTruncateSize();
         if (DEBUG) {
             Log.d(TAG, "Truncate threshold=" + threshold);
@@ -390,12 +383,17 @@
                 + threshold + "; truncating");
         try {
             executeForString("PRAGMA wal_checkpoint(TRUNCATE)", null, null);
+            mConfiguration.shouldTruncateWalFile = false;
         } catch (SQLiteException e) {
             Log.w(TAG, "Failed to truncate the -wal file", e);
         }
     }
 
-    private void setSyncMode(String newValue) {
+    private void setSyncMode(@SQLiteDatabase.SyncMode String newValue) {
+        if (TextUtils.isEmpty(newValue)) {
+            // No change to the sync mode is intended
+            return;
+        }
         String value = executeForString("PRAGMA synchronous", null, null);
         if (!canonicalizeSyncMode(value).equalsIgnoreCase(
                 canonicalizeSyncMode(newValue))) {
@@ -403,16 +401,21 @@
         }
     }
 
-    private static String canonicalizeSyncMode(String value) {
+    private static @SQLiteDatabase.SyncMode String canonicalizeSyncMode(String value) {
         switch (value) {
-            case "0": return "OFF";
-            case "1": return "NORMAL";
-            case "2": return "FULL";
+            case "0": return SQLiteDatabase.SYNC_MODE_OFF;
+            case "1": return SQLiteDatabase.SYNC_MODE_NORMAL;
+            case "2": return SQLiteDatabase.SYNC_MODE_FULL;
+            case "3": return SQLiteDatabase.SYNC_MODE_EXTRA;
         }
         return value;
     }
 
-    private void setJournalMode(String newValue) {
+    private void setJournalMode(@SQLiteDatabase.JournalMode String newValue) {
+        if (TextUtils.isEmpty(newValue)) {
+            // No change to the journal mode is intended
+            return;
+        }
         String value = executeForString("PRAGMA journal_mode", null, null);
         if (!value.equalsIgnoreCase(newValue)) {
             try {
@@ -565,9 +568,6 @@
         // Remember what changed.
         boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled
                 != mConfiguration.foreignKeyConstraintsEnabled;
-        boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags)
-                & (SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING
-                | SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL)) != 0;
         boolean localeChanged = !configuration.locale.equals(mConfiguration.locale);
         boolean customScalarFunctionsChanged = !configuration.customScalarFunctions
                 .equals(mConfiguration.customScalarFunctions);
@@ -586,9 +586,19 @@
         if (foreignKeyModeChanged) {
             setForeignKeyModeFromConfiguration();
         }
-        if (walModeChanged) {
-            setWalModeFromConfiguration();
+
+        boolean journalModeChanged = !configuration.resolveJournalMode().equalsIgnoreCase(
+                mConfiguration.resolveJournalMode());
+        if (journalModeChanged) {
+            setJournalFromConfiguration();
         }
+
+        boolean syncModeChanged =
+                !configuration.resolveSyncMode().equalsIgnoreCase(mConfiguration.resolveSyncMode());
+        if (syncModeChanged) {
+            setSyncModeFromConfiguration();
+        }
+
         if (localeChanged) {
             setLocaleFromConfiguration();
         }
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
index 852f8f2..d3ad6bb 100644
--- a/core/java/android/database/sqlite/SQLiteConnectionPool.java
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -106,7 +106,11 @@
     @GuardedBy("mLock")
     private IdleConnectionHandler mIdleConnectionHandler;
 
-    private final AtomicLong mTotalExecutionTimeCounter = new AtomicLong(0);
+    // whole execution time for this connection in milliseconds.
+    private final AtomicLong mTotalStatementsTime = new AtomicLong(0);
+
+    // total statements executed by this connection
+    private final AtomicLong mTotalStatementsCount = new AtomicLong(0);
 
     // Describes what should happen to an acquired connection when it is returned to the pool.
     enum AcquiredConnectionStatus {
@@ -286,8 +290,11 @@
         synchronized (mLock) {
             throwIfClosedLocked();
 
-            boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags)
-                    & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0;
+            boolean isWalCurrentMode = mConfiguration.resolveJournalMode().equalsIgnoreCase(
+                    SQLiteDatabase.JOURNAL_MODE_WAL);
+            boolean isWalNewMode = configuration.resolveJournalMode().equalsIgnoreCase(
+                    SQLiteDatabase.JOURNAL_MODE_WAL);
+            boolean walModeChanged = isWalCurrentMode ^ isWalNewMode;
             if (walModeChanged) {
                 // WAL mode can only be changed if there are no acquired connections
                 // because we need to close all but the primary connection first.
@@ -536,7 +543,8 @@
     }
 
     void onStatementExecuted(long executionTimeMs) {
-        mTotalExecutionTimeCounter.addAndGet(executionTimeMs);
+        mTotalStatementsTime.addAndGet(executionTimeMs);
+        mTotalStatementsCount.incrementAndGet();
     }
 
     // Can't throw.
@@ -1037,8 +1045,7 @@
     }
 
     private void setMaxConnectionPoolSizeLocked() {
-        if (!mConfiguration.isInMemoryDb()
-                && (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) {
+        if (mConfiguration.resolveJournalMode().equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_WAL)) {
             mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize();
         } else {
             // We don't actually need to always restrict the connection pool size to 1
@@ -1117,11 +1124,18 @@
             printer.println("Connection pool for " + mConfiguration.path + ":");
             printer.println("  Open: " + mIsOpen);
             printer.println("  Max connections: " + mMaxConnectionPoolSize);
-            printer.println("  Total execution time: " + mTotalExecutionTimeCounter);
+            printer.println("  Total execution time (ms): " + mTotalStatementsTime);
+            printer.println("  Total statements executed: " + mTotalStatementsCount);
+            if (mTotalStatementsCount.get() > 0) {
+                // Avoid division by 0 by filtering out logs where there are no statements executed.
+                printer.println("  Average time per statement (ms): "
+                        + mTotalStatementsTime.get() / mTotalStatementsCount.get());
+            }
             printer.println("  Configuration: openFlags=" + mConfiguration.openFlags
                     + ", isLegacyCompatibilityWalEnabled=" + isCompatibilityWalEnabled
-                    + ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.journalMode)
-                    + ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.syncMode));
+                    + ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.resolveJournalMode())
+                    + ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.resolveSyncMode()));
+            printer.println("  IsReadOnlyDatabase=" + mConfiguration.isReadOnlyDatabase());
 
             if (isCompatibilityWalEnabled) {
                 printer.println("  Compatibility WAL enabled: wal_syncmode="
@@ -1182,6 +1196,14 @@
         }
     }
 
+    public long getTotalStatementsTime() {
+        return mTotalStatementsTime.get();
+    }
+
+    public long getTotalStatementsCount() {
+        return mTotalStatementsCount.get();
+    }
+
     @Override
     public String toString() {
         return "SQLiteConnectionPool: " + mConfiguration.path;
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 9684ece..0d0615a 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -20,6 +20,8 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -42,11 +44,8 @@
 import android.util.Log;
 import android.util.Pair;
 import android.util.Printer;
-
 import com.android.internal.util.Preconditions;
-
 import dalvik.system.CloseGuard;
-
 import java.io.File;
 import java.io.FileFilter;
 import java.io.IOException;
@@ -290,15 +289,182 @@
      */
     public static final int MAX_SQL_CACHE_SIZE = 100;
 
-    private SQLiteDatabase(final String path, final int openFlags,
-            CursorFactory cursorFactory, DatabaseErrorHandler errorHandler,
+    /**
+     * @hide
+     */
+    @StringDef(prefix = {"JOURNAL_MODE_"},
+            value =
+                    {
+                            JOURNAL_MODE_WAL,
+                            JOURNAL_MODE_PERSIST,
+                            JOURNAL_MODE_TRUNCATE,
+                            JOURNAL_MODE_MEMORY,
+                            JOURNAL_MODE_DELETE,
+                            JOURNAL_MODE_OFF,
+                    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface JournalMode {}
+
+    /**
+     * The {@code WAL} journaling mode uses a write-ahead log instead of a rollback journal to
+     * implement transactions. The WAL journaling mode is persistent; after being set it stays
+     * in effect across multiple database connections and after closing and reopening the database.
+     *
+     * Performance Considerations:
+     * This mode is recommended when the goal is to improve write performance or parallel read/write
+     * performance. However, it is important to note that WAL introduces checkpoints which commit
+     * all transactions that have not been synced to the database thus to maximize read performance
+     * and lower checkpointing cost a small journal size is recommended. However, other modes such
+     * as {@code DELETE} will not perform checkpoints, so it is a trade off that needs to be
+     * considered as part of the decision of which journal mode to use.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_journal_mode>here</a> for more
+     * details.</p>
+     */
+    public static final String JOURNAL_MODE_WAL = "WAL";
+
+    /**
+     * The {@code PERSIST} journaling mode prevents the rollback journal from being deleted at the
+     * end of each transaction. Instead, the header of the journal is overwritten with zeros.
+     * This will prevent other database connections from rolling the journal back.
+     *
+     * This mode is useful as an optimization on platforms where deleting or truncating a file is
+     * much more expensive than overwriting the first block of a file with zeros.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_journal_mode>here</a> for more
+     * details.</p>
+     */
+    public static final String JOURNAL_MODE_PERSIST = "PERSIST";
+
+    /**
+     * The {@code TRUNCATE} journaling mode commits transactions by truncating the rollback journal
+     * to zero-length instead of deleting it. On many systems, truncating a file is much faster than
+     * deleting the file since the containing directory does not need to be changed.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_journal_mode>here</a> for more
+     * details.</p>
+     */
+    public static final String JOURNAL_MODE_TRUNCATE = "TRUNCATE";
+
+    /**
+     * The {@code MEMORY} journaling mode stores the rollback journal in volatile RAM.
+     * This saves disk I/O but at the expense of database safety and integrity. If the application
+     * using SQLite crashes in the middle of a transaction when the MEMORY journaling mode is set,
+     * then the database file will very likely go corrupt.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_journal_mode>here</a> for more
+     * details.</p>
+     */
+    public static final String JOURNAL_MODE_MEMORY = "MEMORY";
+
+    /**
+     * The {@code DELETE} journaling mode is the normal behavior. In the DELETE mode, the rollback
+     * journal is deleted at the conclusion of each transaction.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_journal_mode>here</a> for more
+     * details.</p>
+     */
+    public static final String JOURNAL_MODE_DELETE = "DELETE";
+
+    /**
+     * The {@code OFF} journaling mode disables the rollback journal completely. No rollback journal
+     * is ever created and hence there is never a rollback journal to delete. The OFF journaling
+     * mode disables the atomic commit and rollback capabilities of SQLite. The ROLLBACK command
+     * behaves in an undefined way thus applications must avoid using the ROLLBACK command.
+     * If the application crashes in the middle of a transaction, then the database file will very
+     * likely go corrupt.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_journal_mode>here</a> for more
+     * details.</p>
+     */
+    public static final String JOURNAL_MODE_OFF = "OFF";
+
+    /**
+     * @hide
+     */
+    @StringDef(prefix = {"SYNC_MODE_"},
+            value =
+                    {
+                            SYNC_MODE_EXTRA,
+                            SYNC_MODE_FULL,
+                            SYNC_MODE_NORMAL,
+                            SYNC_MODE_OFF,
+                    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SyncMode {}
+
+    /**
+     * The {@code EXTRA} sync mode is like {@code FULL} sync mode with the addition that the
+     * directory containing a rollback journal is synced after that journal is unlinked to commit a
+     * transaction in {@code DELETE} journal mode.
+     *
+     * {@code EXTRA} provides additional durability if the commit is followed closely by a
+     * power loss.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_synchronous>here</a> for more
+     * details.</p>
+     */
+    @SuppressLint("IntentName") public static final String SYNC_MODE_EXTRA = "EXTRA";
+
+    /**
+     * In {@code FULL} sync mode the SQLite database engine will use the xSync method of the VFS
+     * to ensure that all content is safely written to the disk surface prior to continuing.
+     * This ensures that an operating system crash or power failure will not corrupt the database.
+     * {@code FULL} is very safe, but it is also slower.
+     *
+     * {@code FULL} is the most commonly used synchronous setting when not in WAL mode.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_synchronous>here</a> for more
+     * details.</p>
+     */
+    public static final String SYNC_MODE_FULL = "FULL";
+
+    /**
+     * The {@code NORMAL} sync mode, the SQLite database engine will still sync at the most critical
+     * moments, but less often than in {@code FULL} mode. There is a very small chance that a
+     * power failure at the wrong time could corrupt the database in {@code DELETE} journal mode on
+     * an older filesystem.
+     *
+     * {@code WAL} journal mode is safe from corruption with {@code NORMAL} sync mode, and probably
+     * {@code DELETE} sync mode is safe too on modern filesystems. WAL mode is always consistent
+     * with {@code NORMAL} sync mode, but WAL mode does lose durability. A transaction committed in
+     * WAL mode with {@code NORMAL} might roll back following a power loss or system crash.
+     * Transactions are durable across application crashes regardless of the synchronous setting
+     * or journal mode.
+     *
+     * The {@code NORMAL} sync mode is a good choice for most applications running in WAL mode.
+     *
+     * <p>Caveat: Even though this sync mode is safe Be careful when using {@code NORMAL} sync mode
+     * when dealing with data dependencies between multiple databases, unless those databases use
+     * the same durability or are somehow synced, there could be corruption.</p>
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_synchronous>here</a> for more
+     * details.</p>
+     */
+    public static final String SYNC_MODE_NORMAL = "NORMAL";
+
+    /**
+     * In {@code OFF} sync mode SQLite continues without syncing as soon as it has handed data off
+     * to the operating system. If the application running SQLite crashes, the data will be safe,
+     * but the database might become corrupted if the operating system crashes or the computer loses
+     * power before that data has been written to the disk surface. On the other hand, commits can
+     * be orders of magnitude faster with synchronous {@code OFF}.
+     *
+     * <p> See <a href=https://www.sqlite.org/pragma.html#pragma_synchronous>here</a> for more
+     * details.</p>
+     */
+    public static final String SYNC_MODE_OFF = "OFF";
+
+    private SQLiteDatabase(@Nullable final String path, @Nullable final int openFlags,
+            @Nullable CursorFactory cursorFactory, @Nullable DatabaseErrorHandler errorHandler,
             int lookasideSlotSize, int lookasideSlotCount, long idleConnectionTimeoutMs,
-            String journalMode, String syncMode) {
+            @Nullable String journalMode, @Nullable String syncMode) {
         mCursorFactory = cursorFactory;
         mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
         mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);
         mConfigurationLocked.lookasideSlotSize = lookasideSlotSize;
         mConfigurationLocked.lookasideSlotCount = lookasideSlotCount;
+
         // Disable lookaside allocator on low-RAM devices
         if (ActivityManager.isLowRamDeviceStatic()) {
             mConfigurationLocked.lookasideSlotCount = 0;
@@ -316,11 +482,11 @@
             }
         }
         mConfigurationLocked.idleConnectionTimeoutMs = effectiveTimeoutMs;
-        mConfigurationLocked.journalMode = journalMode;
-        mConfigurationLocked.syncMode = syncMode;
         if (SQLiteCompatibilityWalFlags.isLegacyCompatibilityWalEnabled()) {
             mConfigurationLocked.openFlags |= ENABLE_LEGACY_COMPATIBILITY_WAL;
         }
+        mConfigurationLocked.journalMode = journalMode;
+        mConfigurationLocked.syncMode = syncMode;
     }
 
     @Override
@@ -2191,7 +2357,8 @@
         synchronized (mLock) {
             throwIfNotOpenLocked();
 
-            if ((mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0) {
+            if (mConfigurationLocked.resolveJournalMode().equalsIgnoreCase(
+                        SQLiteDatabase.JOURNAL_MODE_WAL)) {
                 return true;
             }
 
@@ -2241,11 +2408,9 @@
             throwIfNotOpenLocked();
 
             final int oldFlags = mConfigurationLocked.openFlags;
-            final boolean walEnabled = (oldFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0;
-            final boolean compatibilityWalEnabled =
-                    (oldFlags & ENABLE_LEGACY_COMPATIBILITY_WAL) != 0;
             // WAL was never enabled for this database, so there's nothing left to do.
-            if (!walEnabled && !compatibilityWalEnabled) {
+            if (!mConfigurationLocked.resolveJournalMode().equalsIgnoreCase(
+                        SQLiteDatabase.JOURNAL_MODE_WAL)) {
                 return;
             }
 
@@ -2275,7 +2440,8 @@
         synchronized (mLock) {
             throwIfNotOpenLocked();
 
-            return (mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0;
+            return mConfigurationLocked.resolveJournalMode().equalsIgnoreCase(
+                    SQLiteDatabase.JOURNAL_MODE_WAL);
         }
     }
 
@@ -2309,6 +2475,20 @@
         return databases;
     }
 
+    private static ArrayList<SQLiteConnectionPool> getActiveDatabasePools() {
+        ArrayList<SQLiteConnectionPool> connectionPools = new ArrayList<SQLiteConnectionPool>();
+        synchronized (sActiveDatabases) {
+            for (SQLiteDatabase db : sActiveDatabases.keySet()) {
+                synchronized (db.mLock) {
+                    if (db.mConnectionPoolLocked != null) {
+                        connectionPools.add(db.mConnectionPoolLocked);
+                    }
+                }
+            }
+        }
+        return connectionPools;
+    }
+
     /**
      * Dump detailed information about all open databases in the current process.
      * Used by bug report.
@@ -2317,8 +2497,45 @@
         // Use this ArraySet to collect file paths.
         final ArraySet<String> directories = new ArraySet<>();
 
-        for (SQLiteDatabase db : getActiveDatabases()) {
-            db.dump(printer, verbose, isSystem, directories);
+        // Accounting across all databases
+        long totalStatementsTimeInMs = 0;
+        long totalStatementsCount = 0;
+
+        ArrayList<SQLiteConnectionPool> activeConnectionPools = getActiveDatabasePools();
+
+        activeConnectionPools.sort(
+                (a, b) -> Long.compare(b.getTotalStatementsCount(), a.getTotalStatementsCount()));
+        for (SQLiteConnectionPool dbPool : activeConnectionPools) {
+            dbPool.dump(printer, verbose, directories);
+            totalStatementsTimeInMs += dbPool.getTotalStatementsTime();
+            totalStatementsCount += dbPool.getTotalStatementsCount();
+        }
+
+        if (totalStatementsCount > 0) {
+            // Only print when there is information available
+
+            // Sorted statements per database
+            printer.println("Statements Executed per Database");
+            for (SQLiteConnectionPool dbPool : activeConnectionPools) {
+                printer.println(
+                        "  " + dbPool.getPath() + " :    " + dbPool.getTotalStatementsCount());
+            }
+            printer.println("");
+            printer.println(
+                    "Total Statements Executed for all Active Databases: " + totalStatementsCount);
+
+            // Sorted execution time per database
+            activeConnectionPools.sort(
+                    (a, b) -> Long.compare(b.getTotalStatementsTime(), a.getTotalStatementsTime()));
+            printer.println("");
+            printer.println("");
+            printer.println("Statement Time per Database (ms)");
+            for (SQLiteConnectionPool dbPool : activeConnectionPools) {
+                printer.println(
+                        "  " + dbPool.getPath() + " :    " + dbPool.getTotalStatementsTime());
+            }
+            printer.println("Total Statements Time for all Active Databases (ms): "
+                    + totalStatementsTimeInMs);
         }
 
         // Dump DB files in the directories.
@@ -2331,15 +2548,6 @@
         }
     }
 
-    private void dump(Printer printer, boolean verbose, boolean isSystem, ArraySet directories) {
-        synchronized (mLock) {
-            if (mConnectionPoolLocked != null) {
-                printer.println("");
-                mConnectionPoolLocked.dump(printer, verbose, directories);
-            }
-        }
-    }
-
     private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) {
         pw.println("");
         pw.println("Database files in " + dir.getAbsolutePath() + ":");
@@ -2598,9 +2806,7 @@
 
         /**
          * Returns <a href="https://sqlite.org/pragma.html#pragma_journal_mode">journal mode</a>.
-         * This journal mode will only be used if {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING}
-         * flag is not set, otherwise a platform will use "WAL" journal mode.
-         * @see Builder#setJournalMode(String)
+         * set via {@link Builder#setJournalMode(String)}.
          */
         @Nullable
         public String getJournalMode() {
@@ -2799,25 +3005,28 @@
                 return this;
             }
 
-
             /**
              * Sets <a href="https://sqlite.org/pragma.html#pragma_journal_mode">journal mode</a>
-             * to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag is not set.
+             * to use.
+             *
+             * <p>Note: If journal mode is not set, the platform will use a manufactured-specified
+             * default which can vary across devices.
              */
             @NonNull
-            public Builder setJournalMode(@NonNull  String journalMode) {
+            public Builder setJournalMode(@JournalMode @NonNull String journalMode) {
                 Objects.requireNonNull(journalMode);
                 mJournalMode = journalMode;
                 return this;
             }
 
-            /**w
+            /**
              * Sets <a href="https://sqlite.org/pragma.html#pragma_synchronous">synchronous mode</a>
-             * .
-             * @return
+             *
+             * <p>Note: If sync mode is not set, the platform will use a manufactured-specified
+             * default which can vary across devices.
              */
             @NonNull
-            public Builder setSynchronousMode(@NonNull String syncMode) {
+            public Builder setSynchronousMode(@SyncMode @NonNull String syncMode) {
                 Objects.requireNonNull(syncMode);
                 mSyncMode = syncMode;
                 return this;
diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
index 21c21c9..8305843 100644
--- a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
+++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
@@ -17,9 +17,9 @@
 package android.database.sqlite;
 
 import android.compat.annotation.UnsupportedAppUsage;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Pair;
-
 import java.util.ArrayList;
 import java.util.Locale;
 import java.util.function.BinaryOperator;
@@ -132,14 +132,16 @@
      * Journal mode to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} is not set.
      * <p>Default is returned by {@link SQLiteGlobal#getDefaultJournalMode()}
      */
-    public String journalMode;
+    public @SQLiteDatabase.JournalMode String journalMode;
 
     /**
      * Synchronous mode to use.
      * <p>Default is returned by {@link SQLiteGlobal#getDefaultSyncMode()}
      * or {@link SQLiteGlobal#getWALSyncMode()} depending on journal mode
      */
-    public String syncMode;
+    public @SQLiteDatabase.SyncMode String syncMode;
+
+    public boolean shouldTruncateWalFile;
 
     /**
      * Creates a database configuration with the required parameters for opening a
@@ -217,6 +219,10 @@
         return path.equalsIgnoreCase(MEMORY_DB_PATH);
     }
 
+    public boolean isReadOnlyDatabase() {
+        return (openFlags & SQLiteDatabase.OPEN_READONLY) != 0;
+    }
+
     boolean isLegacyCompatibilityWalEnabled() {
         return journalMode == null && syncMode == null
                 && (openFlags & SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL) != 0;
@@ -232,4 +238,81 @@
     boolean isLookasideConfigSet() {
         return lookasideSlotCount >= 0 && lookasideSlotSize >= 0;
     }
+
+    /**
+     * Resolves the journal mode that should be used when opening a connection to the database.
+     *
+     * Note: assumes openFlags have already been set.
+     *
+     * @return Resolved journal mode that should be used for this database connection or an empty
+     * string if no journal mode should be set.
+     */
+    public @SQLiteDatabase.JournalMode String resolveJournalMode() {
+        if (isReadOnlyDatabase()) {
+            // No need to specify a journal mode when only reading.
+            return "";
+        }
+
+        if (isInMemoryDb()) {
+            if (journalMode != null
+                    && journalMode.equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_OFF)) {
+                return SQLiteDatabase.JOURNAL_MODE_OFF;
+            }
+            return SQLiteDatabase.JOURNAL_MODE_MEMORY;
+        }
+
+        shouldTruncateWalFile = false;
+
+        if (isWalEnabledInternal()) {
+            shouldTruncateWalFile = true;
+            return SQLiteDatabase.JOURNAL_MODE_WAL;
+        } else {
+            // WAL is not explicitly set so use requested journal mode or platform default
+            return this.journalMode != null ? this.journalMode
+                                            : SQLiteGlobal.getDefaultJournalMode();
+        }
+    }
+
+    /**
+     * Resolves the sync mode that should be used when opening a connection to the database.
+     *
+     * Note: assumes openFlags have already been set.
+     * @return Resolved journal mode that should be used for this database connection or null
+     * if no journal mode should be set.
+     */
+    public @SQLiteDatabase.SyncMode String resolveSyncMode() {
+        if (isReadOnlyDatabase()) {
+            // No sync mode will be used since database will be only used for reading.
+            return "";
+        }
+
+        if (isInMemoryDb()) {
+            // No sync mode will be used since database will be in volatile memory
+            return "";
+        }
+
+        if (!TextUtils.isEmpty(syncMode)) {
+            return syncMode;
+        }
+
+        if (isWalEnabledInternal()) {
+            if (isLegacyCompatibilityWalEnabled()) {
+                return SQLiteCompatibilityWalFlags.getWALSyncMode();
+            } else {
+                return SQLiteGlobal.getDefaultSyncMode();
+            }
+        } else {
+            return SQLiteGlobal.getDefaultSyncMode();
+        }
+    }
+
+    private boolean isWalEnabledInternal() {
+        final boolean walEnabled = (openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0;
+        // Use compatibility WAL unless an app explicitly set journal/synchronous mode
+        // or DISABLE_COMPATIBILITY_WAL flag is set
+        final boolean isCompatibilityWalEnabled = isLegacyCompatibilityWalEnabled();
+        return walEnabled || isCompatibilityWalEnabled
+                || (journalMode != null
+                        && journalMode.equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_WAL));
+    }
 }
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
index 1afa0f8..b84a8d2 100644
--- a/core/java/android/database/sqlite/SQLiteDebug.java
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -70,7 +70,8 @@
         /**
          * True to enable database performance testing instrumentation.
          */
-        public static final boolean DEBUG_LOG_SLOW_QUERIES = Build.IS_DEBUGGABLE;
+        public static final boolean DEBUG_LOG_SLOW_QUERIES =
+                Log.isLoggable("SQLiteSlowQueries", Log.VERBOSE);
 
         private static final String SLOW_QUERY_THRESHOLD_PROP = "db.log.slow_query_threshold";
 
diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java
index 5e2875d..28964fd 100644
--- a/core/java/android/database/sqlite/SQLiteGlobal.java
+++ b/core/java/android/database/sqlite/SQLiteGlobal.java
@@ -84,7 +84,7 @@
     /**
      * Gets the default journal mode when WAL is not in use.
      */
-    public static String getDefaultJournalMode() {
+    public static @SQLiteDatabase.JournalMode String getDefaultJournalMode() {
         return SystemProperties.get("debug.sqlite.journalmode",
                 Resources.getSystem().getString(
                 com.android.internal.R.string.db_default_journal_mode));
@@ -102,7 +102,7 @@
     /**
      * Gets the default database synchronization mode when WAL is not in use.
      */
-    public static String getDefaultSyncMode() {
+    public static @SQLiteDatabase.SyncMode String getDefaultSyncMode() {
         // Use the FULL synchronous mode for system processes by default.
         String defaultMode = sDefaultSyncMode;
         if (defaultMode != null) {
@@ -116,7 +116,7 @@
     /**
      * Gets the database synchronization mode when in WAL mode.
      */
-    public static String getWALSyncMode() {
+    public static @SQLiteDatabase.SyncMode String getWALSyncMode() {
         // Use the FULL synchronous mode for system processes by default.
         String defaultMode = sDefaultSyncMode;
         if (defaultMode != null) {
diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java
index 43ef33e..28046c5 100644
--- a/core/java/android/hardware/biometrics/BiometricConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricConstants.java
@@ -151,6 +151,12 @@
     int BIOMETRIC_ERROR_RE_ENROLL = 16;
 
     /**
+     * The privacy setting has been enabled and will block use of the sensor.
+     * @hide
+     */
+    int BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED = 18;
+
+    /**
      * This constant is only used by SystemUI. It notifies SystemUI that authentication was paused
      * because the authentication attempt was unsuccessful.
      * @hide
diff --git a/core/java/android/hardware/biometrics/BiometricFaceConstants.java b/core/java/android/hardware/biometrics/BiometricFaceConstants.java
index fe43c83..fd46f24 100644
--- a/core/java/android/hardware/biometrics/BiometricFaceConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricFaceConstants.java
@@ -69,7 +69,7 @@
             BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL,
             BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED,
             BIOMETRIC_ERROR_RE_ENROLL,
-            FACE_ERROR_UNKNOWN
+            FACE_ERROR_UNKNOWN,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface FaceError {}
diff --git a/core/java/android/net/OWNERS b/core/java/android/net/OWNERS
index f55bcd3..b989488 100644
--- a/core/java/android/net/OWNERS
+++ b/core/java/android/net/OWNERS
@@ -2,5 +2,6 @@
 
 include platform/frameworks/base:/services/core/java/com/android/server/net/OWNERS
 
+per-file **IpSec* = file:/services/core/java/com/android/server/vcn/OWNERS
 per-file SSL*,Uri*,Url* = prb@google.com,oth@google.com,narayan@google.com,ngeoffray@google.com
 per-file SntpClient* = file:/services/core/java/com/android/server/timedetector/OWNERS
diff --git a/core/java/android/net/vcn/VcnCellUnderlyingNetworkPriority.java b/core/java/android/net/vcn/VcnCellUnderlyingNetworkPriority.java
index 6b33e4f..50a6bfc 100644
--- a/core/java/android/net/vcn/VcnCellUnderlyingNetworkPriority.java
+++ b/core/java/android/net/vcn/VcnCellUnderlyingNetworkPriority.java
@@ -29,6 +29,7 @@
 import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.util.PersistableBundleUtils;
 
 import java.util.ArrayList;
@@ -200,6 +201,15 @@
                 && mRequireOpportunistic == rhs.mRequireOpportunistic;
     }
 
+    /** @hide */
+    @Override
+    void dumpTransportSpecificFields(IndentingPrintWriter pw) {
+        pw.println("mAllowedNetworkPlmnIds: " + mAllowedNetworkPlmnIds.toString());
+        pw.println("mAllowedSpecificCarrierIds: " + mAllowedSpecificCarrierIds.toString());
+        pw.println("mAllowRoaming: " + mAllowRoaming);
+        pw.println("mRequireOpportunistic: " + mRequireOpportunistic);
+    }
+
     /** This class is used to incrementally build WifiNetworkPriority objects. */
     public static class Builder extends VcnUnderlyingNetworkPriority.Builder<Builder> {
         @NonNull private final Set<String> mAllowedNetworkPlmnIds = new ArraySet<>();
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index de4ada2..31e38c0 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -160,7 +160,9 @@
                 TimeUnit.MINUTES.toMillis(15)
             };
 
-    private static final LinkedHashSet<VcnUnderlyingNetworkPriority>
+    /** @hide */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static final LinkedHashSet<VcnUnderlyingNetworkPriority>
             DEFAULT_UNDERLYING_NETWORK_PRIORITIES = new LinkedHashSet<>();
 
     static {
diff --git a/core/java/android/net/vcn/VcnUnderlyingNetworkPriority.java b/core/java/android/net/vcn/VcnUnderlyingNetworkPriority.java
index 82f6ae7..551f757 100644
--- a/core/java/android/net/vcn/VcnUnderlyingNetworkPriority.java
+++ b/core/java/android/net/vcn/VcnUnderlyingNetworkPriority.java
@@ -21,8 +21,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.PersistableBundle;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -37,10 +39,17 @@
     /** @hide */
     protected static final int NETWORK_PRIORITY_TYPE_CELL = 2;
 
-    /** Denotes that network quality needs to be OK */
-    public static final int NETWORK_QUALITY_OK = 10000;
     /** Denotes that any network quality is acceptable */
-    public static final int NETWORK_QUALITY_ANY = Integer.MAX_VALUE;
+    public static final int NETWORK_QUALITY_ANY = 0;
+    /** Denotes that network quality needs to be OK */
+    public static final int NETWORK_QUALITY_OK = 100000;
+
+    private static final SparseArray<String> NETWORK_QUALITY_TO_STRING_MAP = new SparseArray<>();
+
+    static {
+        NETWORK_QUALITY_TO_STRING_MAP.put(NETWORK_QUALITY_ANY, "NETWORK_QUALITY_ANY");
+        NETWORK_QUALITY_TO_STRING_MAP.put(NETWORK_QUALITY_OK, "NETWORK_QUALITY_OK");
+    }
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -125,6 +134,28 @@
                 && mAllowMetered == rhs.mAllowMetered;
     }
 
+    /** @hide */
+    abstract void dumpTransportSpecificFields(IndentingPrintWriter pw);
+
+    /**
+     * Dumps the state of this record for logging and debugging purposes.
+     *
+     * @hide
+     */
+    public void dump(IndentingPrintWriter pw) {
+        pw.println(this.getClass().getSimpleName() + ":");
+        pw.increaseIndent();
+
+        pw.println(
+                "mNetworkQuality: "
+                        + NETWORK_QUALITY_TO_STRING_MAP.get(
+                                mNetworkQuality, "Invalid value " + mNetworkQuality));
+        pw.println("mAllowMetered: " + mAllowMetered);
+        dumpTransportSpecificFields(pw);
+
+        pw.decreaseIndent();
+    }
+
     /** Retrieve the required network quality. */
     @NetworkQuality
     public int getNetworkQuality() {
diff --git a/core/java/android/net/vcn/VcnWifiUnderlyingNetworkPriority.java b/core/java/android/net/vcn/VcnWifiUnderlyingNetworkPriority.java
index fc7e7e2..2ba9169 100644
--- a/core/java/android/net/vcn/VcnWifiUnderlyingNetworkPriority.java
+++ b/core/java/android/net/vcn/VcnWifiUnderlyingNetworkPriority.java
@@ -22,6 +22,7 @@
 import android.os.PersistableBundle;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.Objects;
 
@@ -81,6 +82,12 @@
         return mSsid == rhs.mSsid;
     }
 
+    /** @hide */
+    @Override
+    void dumpTransportSpecificFields(IndentingPrintWriter pw) {
+        pw.println("mSsid: " + mSsid);
+    }
+
     /** Retrieve the required SSID, or {@code null} if there is no requirement on SSID. */
     @Nullable
     public String getSsid() {
diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java
index 74b814e..c8b4226e 100644
--- a/core/java/android/os/AppZygote.java
+++ b/core/java/android/os/AppZygote.java
@@ -45,6 +45,8 @@
     // Last UID/GID of the range the AppZygote can setuid()/setgid() to
     private final int mZygoteUidGidMax;
 
+    private final int mZygoteRuntimeFlags;
+
     private final Object mLock = new Object();
 
     /**
@@ -56,11 +58,13 @@
 
     private final ApplicationInfo mAppInfo;
 
-    public AppZygote(ApplicationInfo appInfo, int zygoteUid, int uidGidMin, int uidGidMax) {
+    public AppZygote(ApplicationInfo appInfo, int zygoteUid, int uidGidMin, int uidGidMax,
+            int runtimeFlags) {
         mAppInfo = appInfo;
         mZygoteUid = zygoteUid;
         mZygoteUidGidMin = uidGidMin;
         mZygoteUidGidMax = uidGidMax;
+        mZygoteRuntimeFlags = runtimeFlags;
     }
 
     /**
@@ -110,7 +114,7 @@
                     mZygoteUid,
                     mZygoteUid,
                     null,  // gids
-                    0,  // runtimeFlags
+                    mZygoteRuntimeFlags,  // runtimeFlags
                     "app_zygote",  // seInfo
                     abi,  // abi
                     abi, // acceptedAbiList
diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java
index b80b01f..ae12132 100644
--- a/core/java/android/text/Html.java
+++ b/core/java/android/text/Html.java
@@ -858,9 +858,17 @@
         } else if (tag.equalsIgnoreCase("span")) {
             endCssStyle(mSpannableStringBuilder);
         } else if (tag.equalsIgnoreCase("strong")) {
-            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+            Application application = ActivityThread.currentApplication();
+            int fontWeightAdjustment =
+                    application.getResources().getConfiguration().fontWeightAdjustment;
+            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD,
+                    fontWeightAdjustment));
         } else if (tag.equalsIgnoreCase("b")) {
-            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+            Application application = ActivityThread.currentApplication();
+            int fontWeightAdjustment =
+                    application.getResources().getConfiguration().fontWeightAdjustment;
+            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD,
+                    fontWeightAdjustment));
         } else if (tag.equalsIgnoreCase("em")) {
             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
         } else if (tag.equalsIgnoreCase("cite")) {
@@ -1028,8 +1036,11 @@
         // Their ranges should not include the newlines at the end
         Heading h = getLast(text, Heading.class);
         if (h != null) {
+            Application application = ActivityThread.currentApplication();
+            int fontWeightAdjustment =
+                    application.getResources().getConfiguration().fontWeightAdjustment;
             setSpanFromMark(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]),
-                    new StyleSpan(Typeface.BOLD));
+                    new StyleSpan(Typeface.BOLD, fontWeightAdjustment));
         }
 
         endBlockElement(text);
diff --git a/core/java/android/text/style/StyleSpan.java b/core/java/android/text/style/StyleSpan.java
index bdfa700..ba0ac82 100644
--- a/core/java/android/text/style/StyleSpan.java
+++ b/core/java/android/text/style/StyleSpan.java
@@ -17,8 +17,10 @@
 package android.text.style;
 
 import android.annotation.NonNull;
+import android.content.res.Configuration;
 import android.graphics.Paint;
 import android.graphics.Typeface;
+import android.graphics.fonts.FontStyle;
 import android.os.Parcel;
 import android.text.ParcelableSpan;
 import android.text.TextPaint;
@@ -45,6 +47,7 @@
 public class StyleSpan extends MetricAffectingSpan implements ParcelableSpan {
 
     private final int mStyle;
+    private final int mFontWeightAdjustment;
 
     /**
      * Creates a {@link StyleSpan} from a style.
@@ -54,7 +57,23 @@
      *              in {@link Typeface}.
      */
     public StyleSpan(int style) {
+        this(style, Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED);
+    }
+
+    /**
+     * Creates a {@link StyleSpan} from a style and font weight adjustment.
+     *
+     * @param style An integer constant describing the style for this span. Examples
+     *              include bold, italic, and normal. Values are constants defined
+     *              in {@link Typeface}.
+     * @param fontWeightAdjustment An integer describing the adjustment to be made to the font
+     *              weight.
+     * @see Configuration#fontWeightAdjustment This is the adjustment in text font weight
+     * that is used to reflect the current user's preference for increasing font weight.
+     */
+    public StyleSpan(@Typeface.Style int style, int fontWeightAdjustment) {
         mStyle = style;
+        mFontWeightAdjustment = fontWeightAdjustment;
     }
 
     /**
@@ -64,6 +83,7 @@
      */
     public StyleSpan(@NonNull Parcel src) {
         mStyle = src.readInt();
+        mFontWeightAdjustment = src.readInt();
     }
 
     @Override
@@ -91,6 +111,7 @@
     @Override
     public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
         dest.writeInt(mStyle);
+        dest.writeInt(mFontWeightAdjustment);
     }
 
     /**
@@ -100,17 +121,24 @@
         return mStyle;
     }
 
+    /**
+     * Returns the font weight adjustment specified by this span.
+     */
+    public int getFontWeightAdjustment() {
+        return mFontWeightAdjustment;
+    }
+
     @Override
     public void updateDrawState(TextPaint ds) {
-        apply(ds, mStyle);
+        apply(ds, mStyle, mFontWeightAdjustment);
     }
 
     @Override
     public void updateMeasureState(TextPaint paint) {
-        apply(paint, mStyle);
+        apply(paint, mStyle, mFontWeightAdjustment);
     }
 
-    private static void apply(Paint paint, int style) {
+    private static void apply(Paint paint, int style, int fontWeightAdjustment) {
         int oldStyle;
 
         Typeface old = paint.getTypeface();
@@ -129,6 +157,18 @@
             tf = Typeface.create(old, want);
         }
 
+        // Base typeface may already be bolded by auto bold. Bold further.
+        if ((style & Typeface.BOLD) != 0) {
+            if (fontWeightAdjustment != 0
+                    && fontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
+                int newWeight = Math.min(
+                        Math.max(tf.getWeight() + fontWeightAdjustment, FontStyle.FONT_WEIGHT_MIN),
+                        FontStyle.FONT_WEIGHT_MAX);
+                boolean italic = (want & Typeface.ITALIC) != 0;
+                tf = Typeface.create(tf, newWeight, italic);
+            }
+        }
+
         int fake = want & ~tf.getStyle();
 
         if ((fake & Typeface.BOLD) != 0) {
diff --git a/core/java/android/window/ITaskFragmentOrganizerController.aidl b/core/java/android/window/ITaskFragmentOrganizerController.aidl
index 1ad0452..4399207 100644
--- a/core/java/android/window/ITaskFragmentOrganizerController.aidl
+++ b/core/java/android/window/ITaskFragmentOrganizerController.aidl
@@ -44,4 +44,10 @@
      * Unregisters remote animations per transition type for the organizer.
      */
     void unregisterRemoteAnimations(in ITaskFragmentOrganizer organizer);
+
+    /**
+      * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
+      * only occupies a portion of Task bounds.
+      */
+    boolean isActivityEmbedded(in IBinder activityToken);
 }
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index 7e7d370..9c2fde0 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -216,4 +216,17 @@
             return null;
         }
     }
+
+    /**
+     * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
+     * only occupies a portion of Task bounds.
+     * @hide
+     */
+    public boolean isActivityEmbedded(@NonNull IBinder activityToken) {
+        try {
+            return getController().isActivityEmbedded(activityToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index be5dc00..d66f461 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -870,7 +870,8 @@
                                 boolean allowedMinSdk = minDeviceSdk <= Build.VERSION.SDK_INT;
                                 boolean allowedMaxSdk =
                                         maxDeviceSdk == 0 || maxDeviceSdk >= Build.VERSION.SDK_INT;
-                                if (allowedMinSdk && allowedMaxSdk) {
+                                final boolean exists = new File(lfile).exists();
+                                if (allowedMinSdk && allowedMaxSdk && exists) {
                                     int bcpSince = XmlUtils.readIntAttribute(parser,
                                             "on-bootclasspath-since", 0);
                                     int bcpBefore = XmlUtils.readIntAttribute(parser,
@@ -880,6 +881,19 @@
                                                     ? new String[0] : ldependency.split(":"),
                                             bcpSince, bcpBefore);
                                     mSharedLibraries.put(lname, entry);
+                                } else {
+                                    final StringBuilder msg = new StringBuilder(
+                                            "Ignore shared library ").append(lname).append(":");
+                                    if (!allowedMinSdk) {
+                                        msg.append(" min-device-sdk=").append(minDeviceSdk);
+                                    }
+                                    if (!allowedMaxSdk) {
+                                        msg.append(" max-device-sdk=").append(maxDeviceSdk);
+                                    }
+                                    if (!exists) {
+                                        msg.append(" ").append(lfile).append(" does not exist");
+                                    }
+                                    Slog.i(TAG, msg.toString());
                                 }
                             }
                         } else {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 650bd3d..769e667 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1762,6 +1762,8 @@
     <string name="face_setup_notification_title">Set up Face Unlock</string>
     <!-- Contents of a notification that directs the user to set up face unlock by enrolling their face. [CHAR LIMIT=NONE] -->
     <string name="face_setup_notification_content">Unlock your phone by looking at it</string>
+    <!-- Error message indicating that the camera privacy sensor has been turned on [CHAR LIMIT=NONE] -->
+    <string name="face_sensor_privacy_enabled">To use Face Unlock, turn on <b>Camera access</b> in Settings > Privacy</string>
     <!-- Title of a notification that directs the user to enroll a fingerprint. [CHAR LIMIT=NONE] -->
     <string name="fingerprint_setup_notification_title">Set up more ways to unlock</string>
     <!-- Contents of a notification that directs the user to enroll a fingerprint. [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ccb487e..134235d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2587,6 +2587,7 @@
   <java-symbol type="string" name="face_recalibrate_notification_name" />
   <java-symbol type="string" name="face_recalibrate_notification_title" />
   <java-symbol type="string" name="face_recalibrate_notification_content" />
+  <java-symbol type="string" name="face_sensor_privacy_enabled" />
   <java-symbol type="string" name="face_error_unable_to_process" />
   <java-symbol type="string" name="face_error_hw_not_available" />
   <java-symbol type="string" name="face_error_no_space" />
diff --git a/core/tests/bluetoothtests/src/android/bluetooth/BluetoothLeAudioCodecConfigTest.java b/core/tests/bluetoothtests/src/android/bluetooth/BluetoothLeAudioCodecConfigTest.java
index 6471492..c3d707c 100644
--- a/core/tests/bluetoothtests/src/android/bluetooth/BluetoothLeAudioCodecConfigTest.java
+++ b/core/tests/bluetoothtests/src/android/bluetooth/BluetoothLeAudioCodecConfigTest.java
@@ -45,7 +45,6 @@
                 assertEquals("INVALID CODEC", leAudioCodecConfig.getCodecName());
             }
 
-            assertEquals(1, leAudioCodecConfig.getMaxCodecType());
             assertEquals(codecType, leAudioCodecConfig.getCodecType());
         }
     }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 70a3fc7..60cb9d3 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -517,6 +517,8 @@
         <permission name="android.permission.MANAGE_VOICE_KEYPHRASES" />
         <!-- Permission required for ATS test - CarDevicePolicyManagerTest -->
         <permission name="android.permission.LOCK_DEVICE" />
+        <!-- Permission required for CTS test - CtsSafetyCenterTestCases -->
+        <permission name="android.permission.SEND_SAFETY_CENTER_UPDATE" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
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 fe6c7ba..af19bd0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -91,11 +91,13 @@
      */
     public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
             @Nullable Bundle options, @NonNull SplitRule sideRule,
-            @NonNull Consumer<Exception> failureCallback) {
+            @Nullable Consumer<Exception> failureCallback) {
         try {
             mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule);
         } catch (Exception e) {
-            failureCallback.accept(e);
+            if (failureCallback != null) {
+                failureCallback.accept(e);
+            }
         }
     }
 
@@ -858,4 +860,12 @@
                     launchingContainer.getTaskFragmentToken());
         }
     }
+
+    /**
+     * Checks if an activity is embedded and its presentation is customized by a
+     * {@link android.window.TaskFragmentOrganizer} to only occupy a portion of Task bounds.
+     */
+    public boolean isActivityEmbedded(@NonNull Activity activity) {
+        return mPresenter.isActivityEmbedded(activity.getActivityToken());
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index 8c8ef92..46bdf6d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -16,6 +16,7 @@
 
 package androidx.window.extensions.embedding;
 
+import static android.os.Process.THREAD_PRIORITY_DISPLAY;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
@@ -29,7 +30,7 @@
 import android.animation.ValueAnimator;
 import android.graphics.Rect;
 import android.os.Handler;
-import android.os.Looper;
+import android.os.HandlerThread;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.IRemoteAnimationFinishedCallback;
@@ -50,10 +51,14 @@
 class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
 
     private static final String TAG = "TaskFragAnimationRunner";
-    private final Handler mHandler = new Handler(Looper.myLooper());
+    private final Handler mHandler;
     private final TaskFragmentAnimationSpec mAnimationSpec;
 
     TaskFragmentAnimationRunner() {
+        HandlerThread animationThread = new HandlerThread(
+                "androidx.window.extensions.embedding", THREAD_PRIORITY_DISPLAY);
+        animationThread.start();
+        mHandler = animationThread.getThreadHandler();
         mAnimationSpec = new TaskFragmentAnimationSpec(mHandler);
     }
 
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 a1a53bc..4d2d055 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -103,7 +103,7 @@
         ActivityThread activityThread = ActivityThread.currentActivityThread();
         for (IBinder token : mInfo.getActivities()) {
             Activity activity = activityThread.getActivity(token);
-            if (activity != null && !allActivities.contains(activity)) {
+            if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) {
                 allActivities.add(activity);
             }
         }
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index d6678bf..f54ab08 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
index 82e8273..da4bbe8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
@@ -189,7 +189,7 @@
         mEnterSplitButton = findViewById(R.id.enter_split);
         mEnterSplitButton.setAlpha(0);
         mEnterSplitButton.setOnClickListener(v -> {
-            if (mMenuContainer.getAlpha() != 0) {
+            if (mEnterSplitButton.getAlpha() != 0) {
                 enterSplit();
             }
         });
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 e30e6c5..d1feee4 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
@@ -965,11 +965,17 @@
 
     private void updateUnfoldBounds() {
         if (mMainUnfoldController != null && mSideUnfoldController != null) {
-            mMainUnfoldController.onLayoutChanged(getMainStageBounds());
-            mSideUnfoldController.onLayoutChanged(getSideStageBounds());
+            mMainUnfoldController.onLayoutChanged(getMainStageBounds(), getMainStagePosition(),
+                    isLandscape());
+            mSideUnfoldController.onLayoutChanged(getSideStageBounds(), getSideStagePosition(),
+                    isLandscape());
         }
     }
 
+    private boolean isLandscape() {
+        return mSplitLayout.isLandscape();
+    }
+
     /**
      * Populates `wct` with operations that match the split windows to the current layout.
      * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
index e904f6a..0683a25 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
@@ -18,11 +18,15 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+
 import android.animation.RectEvaluator;
 import android.animation.TypeEvaluator;
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.content.Context;
+import android.graphics.Insets;
 import android.graphics.Rect;
 import android.util.SparseArray;
 import android.view.InsetsSource;
@@ -33,6 +37,7 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
 import com.android.wm.shell.unfold.UnfoldBackgroundController;
@@ -161,12 +166,13 @@
      * Called when split screen stage bounds changed
      * @param bounds new bounds for this stage
      */
-    public void onLayoutChanged(Rect bounds) {
+    public void onLayoutChanged(Rect bounds, @SplitPosition int splitPosition,
+            boolean isLandscape) {
         mStageBounds.set(bounds);
 
         for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
             final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
-            context.update();
+            context.update(splitPosition, isLandscape);
         }
     }
 
@@ -195,20 +201,27 @@
         final Rect mEndCropRect = new Rect();
         final Rect mCurrentCropRect = new Rect();
 
+        private @SplitPosition int mSplitPosition = SPLIT_POSITION_UNDEFINED;
+        private boolean mIsLandscape = false;
+
         private AnimationContext(SurfaceControl leash) {
             this.mLeash = leash;
             update();
         }
 
+        private void update(@SplitPosition int splitPosition, boolean isLandscape) {
+            this.mSplitPosition = splitPosition;
+            this.mIsLandscape = isLandscape;
+            update();
+        }
+
         private void update() {
             mStartCropRect.set(mStageBounds);
 
-            if (mTaskbarInsetsSource != null) {
+            boolean taskbarExpanded = isTaskbarExpanded();
+            if (taskbarExpanded) {
                 // Only insets the cropping window with taskbar when taskbar is expanded
-                if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
-                    mStartCropRect.inset(mTaskbarInsetsSource
-                            .calculateVisibleInsets(mStartCropRect));
-                }
+                mStartCropRect.inset(mTaskbarInsetsSource.calculateVisibleInsets(mStartCropRect));
             }
 
             // Offset to surface coordinates as layout bounds are in screen coordinates
@@ -218,7 +231,46 @@
 
             int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
             int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
-            mStartCropRect.inset(margin, margin, margin, margin);
+
+            // Sides adjacent to split bar or task bar are not be animated.
+            Insets margins;
+            if (mIsLandscape) { // Left and right splits.
+                margins = getLandscapeMargins(margin, taskbarExpanded);
+            } else { // Top and bottom splits.
+                margins = getPortraitMargins(margin, taskbarExpanded);
+            }
+            mStartCropRect.inset(margins);
+        }
+
+        private Insets getLandscapeMargins(int margin, boolean taskbarExpanded) {
+            int left = margin;
+            int right = margin;
+            int bottom = taskbarExpanded ? 0 : margin; // Taskbar margin.
+            if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+                right = 0; // Divider margin.
+            } else {
+                left = 0; // Divider margin.
+            }
+            return Insets.of(left, /* top= */ margin, right, bottom);
+        }
+
+        private Insets getPortraitMargins(int margin, boolean taskbarExpanded) {
+            int bottom = margin;
+            int top = margin;
+            if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+                bottom = 0; // Divider margin.
+            } else { // Bottom split.
+                top = 0; // Divider margin.
+                if (taskbarExpanded) {
+                    bottom = 0; // Taskbar margin.
+                }
+            }
+            return Insets.of(/* left= */ margin, top, /* right= */ margin, bottom);
+        }
+
+        private boolean isTaskbarExpanded() {
+            return mTaskbarInsetsSource != null
+                    && mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight;
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt
index 1a3e42c..20a9475 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.apppairs
 
-import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -68,7 +67,7 @@
     @Test
     override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
 
-    @Postsubmit
+    @Presubmit
     @Test
     override fun statusBarLayerRotatesScales() {
         // This test doesn't work in shell transitions because of b/206753786
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
index 3210976..2a53bef 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.pip
 
-import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import androidx.test.filters.FlakyTest
@@ -96,7 +95,7 @@
         super.statusBarLayerRotatesScales()
     }
 
-    @Postsubmit
+    @Presubmit
     @Test
     fun pipWindowInsideDisplay() {
         testSpec.assertWmStart {
@@ -112,7 +111,7 @@
         }
     }
 
-    @Postsubmit
+    @Presubmit
     @Test
     fun pipLayerInsideDisplay() {
         testSpec.assertLayersStart {
@@ -126,7 +125,7 @@
         this.isAppWindowVisible(pipApp.component)
     }
 
-    @Postsubmit
+    @Presubmit
     @Test
     fun pipAppLayerCoversFullScreen() {
         testSpec.assertLayersEnd {
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 85f6789..fb6300c 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
@@ -114,6 +114,7 @@
 
         when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
         when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
+        when(mSplitLayout.isLandscape()).thenReturn(false);
     }
 
     @Test
@@ -168,8 +169,9 @@
 
         mStageCoordinator.onLayoutSizeChanged(mSplitLayout);
 
-        verify(mMainUnfoldController).onLayoutChanged(mBounds2);
-        verify(mSideUnfoldController).onLayoutChanged(mBounds1);
+        verify(mMainUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                false);
+        verify(mSideUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT, false);
     }
 
     @Test
@@ -180,8 +182,10 @@
 
         mStageCoordinator.onLayoutSizeChanged(mSplitLayout);
 
-        verify(mMainUnfoldController).onLayoutChanged(mBounds1);
-        verify(mSideUnfoldController).onLayoutChanged(mBounds2);
+        verify(mMainUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT,
+                false);
+        verify(mSideUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                false);
     }
 
     @Test
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 46aad3f..d721291 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -89,6 +89,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 /**
  * AudioManager provides access to volume and ringer mode control.
@@ -5775,6 +5776,23 @@
     }
 
     /**
+     * Indicate wired accessory connection state change.
+     * @param device {@link AudioDeviceAttributes} of the device to "fake-connect"
+     * @param connected true for connected, false for disconnected
+     * {@hide}
+     */
+    @TestApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void setTestDeviceConnectionState(@NonNull AudioDeviceAttributes device,
+            boolean connected) {
+        try {
+            getService().setTestDeviceConnectionState(device, connected);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Indicate Bluetooth profile connection state change.
      * Configuration changes for A2DP are indicated by having the same <code>newDevice</code> and
      * <code>previousDevice</code>
@@ -7968,6 +7986,231 @@
     }
 
     //---------------------------------------------------------
+    // audio device connection-dependent muting
+    /**
+     * @hide
+     * Mute a set of playback use cases until a given audio device is connected.
+     * Automatically unmute upon connection of the device, or after the given timeout, whichever
+     * happens first.
+     * @param usagesToMute non-empty array of {@link AudioAttributes} usages (for example
+     *                     {@link AudioAttributes#USAGE_MEDIA}) to mute until the
+     *                     device connects
+     * @param device the audio device expected to connect within the timeout duration
+     * @param timeout the maximum amount of time to wait for the device connection
+     * @param timeUnit the unit for the timeout
+     * @throws IllegalStateException when trying to issue the command while another is already in
+     *         progress and hasn't been cancelled by
+     *         {@link #cancelMuteAwaitConnection(AudioDeviceAttributes)}. See
+     *         {@link #getMutingExpectedDevice()} to check if a muting command is active.
+     * @see #registerMuteAwaitConnectionCallback(Executor, AudioManager.MuteAwaitConnectionCallback)
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void muteAwaitConnection(@NonNull int[] usagesToMute,
+            @NonNull AudioDeviceAttributes device,
+            long timeout, @NonNull TimeUnit timeUnit) throws IllegalStateException {
+        if (timeout <= 0) {
+            throw new IllegalArgumentException("Timeout must be greater than 0");
+        }
+        Objects.requireNonNull(usagesToMute);
+        if (usagesToMute.length == 0) {
+            throw new IllegalArgumentException("Array of usages to mute cannot be empty");
+        }
+        Objects.requireNonNull(device);
+        Objects.requireNonNull(timeUnit);
+        try {
+            getService().muteAwaitConnection(usagesToMute, device, timeUnit.toMillis(timeout));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Query which audio device, if any, is causing some playback use cases to be muted until it
+     * connects.
+     * @return the audio device used in
+     *        {@link #muteAwaitConnection(int[], AudioDeviceAttributes, long, TimeUnit)}, or null
+     *        if there is no active muting command (either because the muting command was not issued
+     *        or because it timed out)
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public @Nullable AudioDeviceAttributes getMutingExpectedDevice() {
+        try {
+            return getService().getMutingExpectedDevice();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Cancel a {@link #muteAwaitConnection(int[], AudioDeviceAttributes, long, TimeUnit)}
+     * command.
+     * @param device the device whose connection was expected when the {@code muteAwaitConnection}
+     *               command was issued.
+     * @throws IllegalStateException when trying to issue the command for a device whose connection
+     *         is not anticipated by a previous call to
+     *         {@link #muteAwaitConnection(int[], AudioDeviceAttributes, long, TimeUnit)}
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void cancelMuteAwaitConnection(@NonNull AudioDeviceAttributes device)
+            throws IllegalStateException {
+        Objects.requireNonNull(device);
+        try {
+            getService().cancelMuteAwaitConnection(device);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * A callback class to receive events about the muting and unmuting of playback use cases
+     * conditional on the upcoming connection of an audio device.
+     * @see #registerMuteAwaitConnectionCallback(Executor, AudioManager.MuteAwaitConnectionCallback)
+     */
+    @SystemApi
+    public abstract static class MuteAwaitConnectionCallback {
+
+        /**
+         * An event where the expected audio device connected
+         * @see MuteAwaitConnectionCallback#onUnmutedEvent(int, AudioDeviceAttributes, int[])
+         */
+        public static final int EVENT_CONNECTION = 1;
+        /**
+         * An event where the expected audio device failed connect before the timeout happened
+         * @see MuteAwaitConnectionCallback#onUnmutedEvent(int, AudioDeviceAttributes, int[])
+         */
+        public static final int EVENT_TIMEOUT    = 2;
+        /**
+         * An event where the {@code muteAwaitConnection()} command
+         * was cancelled with {@link #cancelMuteAwaitConnection(AudioDeviceAttributes)}
+         * @see MuteAwaitConnectionCallback#onUnmutedEvent(int, AudioDeviceAttributes, int[])
+         */
+        public static final int EVENT_CANCEL     = 3;
+
+        /** @hide */
+        @IntDef(flag = false, prefix = "EVENT_", value = {
+                EVENT_CONNECTION,
+                EVENT_TIMEOUT,
+                EVENT_CANCEL }
+        )
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface UnmuteEvent {}
+
+        /**
+         * Called when a number of playback use cases are muted in response to a call to
+         * {@link #muteAwaitConnection(int[], AudioDeviceAttributes, long, TimeUnit)}.
+         * @param device the audio device whose connection is expected. Playback use cases are
+         *               unmuted when that device connects
+         * @param mutedUsages an array of {@link AudioAttributes} usages that describe the affected
+         *                    playback use cases.
+         */
+        public void onMutedUntilConnection(
+                @NonNull AudioDeviceAttributes device,
+                @NonNull int[] mutedUsages) {}
+
+        /**
+         * Called when an event occurred that caused playback uses cases to be unmuted
+         * @param unmuteEvent the nature of the event
+         * @param device the device that was expected to connect
+         * @param mutedUsages the array of {@link AudioAttributes} usages that were muted until
+         *                    the event occurred
+         */
+        public void onUnmutedEvent(
+                @UnmuteEvent int unmuteEvent,
+                @NonNull AudioDeviceAttributes device, @NonNull int[] mutedUsages) {}
+    }
+
+
+    /**
+     * @hide
+     * Register a callback to receive updates on the playback muting conditional on a specific
+     * audio device connection.
+     * @param executor the {@link Executor} handling the callback
+     * @param callback the callback to register
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void registerMuteAwaitConnectionCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull MuteAwaitConnectionCallback callback) {
+        synchronized (mMuteAwaitConnectionListenerLock) {
+            final Pair<ArrayList<ListenerInfo<MuteAwaitConnectionCallback>>,
+                    MuteAwaitConnectionDispatcherStub> res =
+                    CallbackUtil.addListener("registerMuteAwaitConnectionCallback",
+                            executor, callback, mMuteAwaitConnectionListeners,
+                            mMuteAwaitConnDispatcherStub,
+                            () -> new MuteAwaitConnectionDispatcherStub(),
+                            stub -> stub.register(true));
+            mMuteAwaitConnectionListeners = res.first;
+            mMuteAwaitConnDispatcherStub = res.second;
+        }
+    }
+
+    /**
+     * @hide
+     * Unregister a previously registered callback for playback muting conditional on device
+     * connection.
+     * @param callback the callback to unregister
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void unregisterMuteAwaitConnectionCallback(
+            @NonNull MuteAwaitConnectionCallback callback) {
+        synchronized (mMuteAwaitConnectionListenerLock) {
+            final Pair<ArrayList<ListenerInfo<MuteAwaitConnectionCallback>>,
+                    MuteAwaitConnectionDispatcherStub> res =
+                    CallbackUtil.removeListener("unregisterMuteAwaitConnectionCallback",
+                            callback, mMuteAwaitConnectionListeners, mMuteAwaitConnDispatcherStub,
+                            stub -> stub.register(false));
+            mMuteAwaitConnectionListeners = res.first;
+            mMuteAwaitConnDispatcherStub = res.second;
+        }
+    }
+
+    private final Object mMuteAwaitConnectionListenerLock = new Object();
+
+    @GuardedBy("mMuteAwaitConnectionListenerLock")
+    private @Nullable ArrayList<ListenerInfo<MuteAwaitConnectionCallback>>
+            mMuteAwaitConnectionListeners;
+
+    @GuardedBy("mMuteAwaitConnectionListenerLock")
+    private MuteAwaitConnectionDispatcherStub mMuteAwaitConnDispatcherStub;
+
+    private final class MuteAwaitConnectionDispatcherStub
+            extends IMuteAwaitConnectionCallback.Stub {
+        public void register(boolean register) {
+            try {
+                getService().registerMuteAwaitConnectionDispatcher(this, register);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        @Override
+        @SuppressLint("GuardedBy") // lock applied inside callListeners method
+        public void dispatchOnMutedUntilConnection(AudioDeviceAttributes device,
+                int[] mutedUsages) {
+            CallbackUtil.callListeners(mMuteAwaitConnectionListeners,
+                    mMuteAwaitConnectionListenerLock,
+                    (listener) -> listener.onMutedUntilConnection(device, mutedUsages));
+        }
+
+        @Override
+        @SuppressLint("GuardedBy") // lock applied inside callListeners method
+        public void dispatchOnUnmutedEvent(int event, AudioDeviceAttributes device,
+                int[] mutedUsages) {
+            CallbackUtil.callListeners(mMuteAwaitConnectionListeners,
+                    mMuteAwaitConnectionListenerLock,
+                    (listener) -> listener.onUnmutedEvent(event, device, mutedUsages));
+        }
+    }
+
+    //---------------------------------------------------------
     // Inner classes
     //--------------------
     /**
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index f15f880..afcbc57 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -32,6 +32,7 @@
 import android.media.IAudioServerStateDispatcher;
 import android.media.ICapturePresetDevicesRoleDispatcher;
 import android.media.ICommunicationDeviceDispatcher;
+import android.media.IMuteAwaitConnectionCallback;
 import android.media.IPlaybackConfigDispatcher;
 import android.media.IRecordingConfigDispatcher;
 import android.media.IRingtonePlayer;
@@ -447,4 +448,16 @@
     boolean isVolumeFixed();
 
     boolean isPstnCallAudioInterceptable();
+
+    oneway void muteAwaitConnection(in int[] usagesToMute, in AudioDeviceAttributes dev,
+            long timeOutMs);
+
+    oneway void cancelMuteAwaitConnection(in AudioDeviceAttributes dev);
+
+    AudioDeviceAttributes getMutingExpectedDevice();
+
+    void registerMuteAwaitConnectionDispatcher(in IMuteAwaitConnectionCallback cb,
+            boolean register);
+
+    void setTestDeviceConnectionState(in AudioDeviceAttributes device, boolean connected);
 }
diff --git a/media/java/android/media/IMuteAwaitConnectionCallback.aidl b/media/java/android/media/IMuteAwaitConnectionCallback.aidl
new file mode 100644
index 0000000..77fc029
--- /dev/null
+++ b/media/java/android/media/IMuteAwaitConnectionCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * 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.media;
+
+import android.media.AudioDeviceAttributes;
+
+/**
+ * AIDL for the AudioService to signal mute events tied to audio device connections.
+ *
+ * {@hide}
+ */
+oneway interface IMuteAwaitConnectionCallback {
+
+    void dispatchOnMutedUntilConnection(in AudioDeviceAttributes device, in int[] mutedUsages);
+
+    void dispatchOnUnmutedEvent(int event, in AudioDeviceAttributes device, in int[] mutedUsages);
+}
diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index 73a821e..94de7fa 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -74,6 +74,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * This class is used to interact with hardware tuners devices.
@@ -248,6 +249,7 @@
 
     private static final int FILTER_CLEANUP_THRESHOLD = 256;
 
+
     /** @hide */
     @IntDef(prefix = "DVR_TYPE_", value = {DVR_TYPE_RECORD, DVR_TYPE_PLAYBACK})
     @Retention(RetentionPolicy.SOURCE)
@@ -304,6 +306,11 @@
     private final Object mOnTuneEventLock = new Object();
     private final Object mScanCallbackLock = new Object();
     private final Object mOnResourceLostListenerLock = new Object();
+    private final ReentrantLock mFrontendLock = new ReentrantLock();
+    private final ReentrantLock mLnbLock = new ReentrantLock();
+    private final ReentrantLock mFrontendCiCamLock = new ReentrantLock();
+    private final ReentrantLock mDemuxLock = new ReentrantLock();
+    private int mRequestedCiCamId;
 
     private Integer mDemuxHandle;
     private Integer mFrontendCiCamHandle;
@@ -391,7 +398,12 @@
 
     /** @hide */
     public List<Integer> getFrontendIds() {
-        return nativeGetFrontendIds();
+        mFrontendLock.lock();
+        try {
+            return nativeGetFrontendIds();
+        } finally {
+            mFrontendLock.unlock();
+        }
     }
 
     /**
@@ -426,13 +438,20 @@
      * @param tuner the Tuner instance to share frontend resource with.
      */
     public void shareFrontendFromTuner(@NonNull Tuner tuner) {
-        mTunerResourceManager.shareFrontend(mClientId, tuner.mClientId);
-        synchronized (mIsSharedFrontend) {
-            mFrontendHandle = tuner.mFrontendHandle;
-            mFrontend = tuner.mFrontend;
-            mIsSharedFrontend = true;
+        acquireTRMSLock("shareFrontendFromTuner()");
+        mFrontendLock.lock();
+        try {
+            mTunerResourceManager.shareFrontend(mClientId, tuner.mClientId);
+            synchronized (mIsSharedFrontend) {
+                mFrontendHandle = tuner.mFrontendHandle;
+                mFrontend = tuner.mFrontend;
+                mIsSharedFrontend = true;
+            }
+            nativeShareFrontend(mFrontend.mId);
+        } finally {
+            releaseTRMSLock();
+            mFrontendLock.unlock();
         }
-        nativeShareFrontend(mFrontend.mId);
     }
 
     /**
@@ -498,39 +517,62 @@
      */
     @Override
     public void close() {
-        releaseAll();
-        TunerUtils.throwExceptionForResult(nativeClose(), "failed to close tuner");
+        acquireTRMSLock("close()");
+        try {
+            releaseAll();
+            TunerUtils.throwExceptionForResult(nativeClose(), "failed to close tuner");
+        } finally {
+            releaseTRMSLock();
+        }
     }
 
     private void releaseAll() {
-        if (mFrontendHandle != null) {
-            synchronized (mIsSharedFrontend) {
-                if (!mIsSharedFrontend) {
-                    int res = nativeCloseFrontend(mFrontendHandle);
-                    if (res != Tuner.RESULT_SUCCESS) {
-                        TunerUtils.throwExceptionForResult(res, "failed to close frontend");
+        mFrontendLock.lock();
+        try {
+            if (mFrontendHandle != null) {
+                synchronized (mIsSharedFrontend) {
+                    if (!mIsSharedFrontend) {
+                        int res = nativeCloseFrontend(mFrontendHandle);
+                        if (res != Tuner.RESULT_SUCCESS) {
+                            TunerUtils.throwExceptionForResult(res, "failed to close frontend");
+                        }
+                        mTunerResourceManager.releaseFrontend(mFrontendHandle, mClientId);
                     }
-                    mTunerResourceManager.releaseFrontend(mFrontendHandle, mClientId);
+                    mIsSharedFrontend = false;
                 }
-                mIsSharedFrontend = false;
+                FrameworkStatsLog
+                        .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
+                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__UNKNOWN);
+                mFrontendHandle = null;
+                mFrontend = null;
             }
-            FrameworkStatsLog
-                    .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
-                    FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__UNKNOWN);
-            mFrontendHandle = null;
-            mFrontend = null;
+        } finally {
+            mFrontendLock.unlock();
         }
-        if (mLnb != null) {
-            mLnb.close();
-        }
-        if (mFrontendCiCamHandle != null) {
-            int result = nativeUnlinkCiCam(mFrontendCiCamId);
-            if (result == RESULT_SUCCESS) {
-                mTunerResourceManager.releaseCiCam(mFrontendCiCamHandle, mClientId);
-                mFrontendCiCamId = null;
-                mFrontendCiCamHandle = null;
+
+        mLnbLock.lock();
+        try {
+            if (mLnb != null) {
+                mLnb.close();
             }
+        } finally {
+            mLnbLock.unlock();
         }
+
+        mFrontendCiCamLock.lock();
+        try {
+            if (mFrontendCiCamHandle != null) {
+                int result = nativeUnlinkCiCam(mFrontendCiCamId);
+                if (result == RESULT_SUCCESS) {
+                    mTunerResourceManager.releaseCiCam(mFrontendCiCamHandle, mClientId);
+                    mFrontendCiCamId = null;
+                    mFrontendCiCamHandle = null;
+                }
+            }
+        } finally {
+            mFrontendCiCamLock.unlock();
+        }
+
         synchronized (mDescramblers) {
             if (!mDescramblers.isEmpty()) {
                 for (Map.Entry<Integer, WeakReference<Descrambler>> d : mDescramblers.entrySet()) {
@@ -543,6 +585,7 @@
                 mDescramblers.clear();
             }
         }
+
         synchronized (mFilters) {
             if (!mFilters.isEmpty()) {
                 for (WeakReference<Filter> weakFilter : mFilters) {
@@ -554,13 +597,19 @@
                 mFilters.clear();
             }
         }
-        if (mDemuxHandle != null) {
-            int res = nativeCloseDemux(mDemuxHandle);
-            if (res != Tuner.RESULT_SUCCESS) {
-                TunerUtils.throwExceptionForResult(res, "failed to close demux");
+
+        mDemuxLock.lock();
+        try {
+            if (mDemuxHandle != null) {
+                int res = nativeCloseDemux(mDemuxHandle);
+                if (res != Tuner.RESULT_SUCCESS) {
+                    TunerUtils.throwExceptionForResult(res, "failed to close demux");
+                }
+                mTunerResourceManager.releaseDemux(mDemuxHandle, mClientId);
+                mDemuxHandle = null;
             }
-            mTunerResourceManager.releaseDemux(mDemuxHandle, mClientId);
-            mDemuxHandle = null;
+        } finally {
+            mDemuxLock.unlock();
         }
 
         mTunerResourceManager.unregisterClientProfile(mClientId);
@@ -763,28 +812,37 @@
      */
     @Result
     public int tune(@NonNull FrontendSettings settings) {
-        final int type = settings.getType();
-        if (mFrontendHandle != null && type != mFrontendType) {
-            Log.e(TAG, "Frontend was opened with type " + mFrontendType + ", new type is " + type);
-            return RESULT_INVALID_STATE;
-        }
-        Log.d(TAG, "Tune to " + settings.getFrequencyLong());
-        mFrontendType = type;
-        if (mFrontendType == FrontendSettings.TYPE_DTMB) {
-            if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
-                    TunerVersionChecker.TUNER_VERSION_1_1, "Tuner with DTMB Frontend")) {
+        mFrontendLock.lock();
+        try {
+            final int type = settings.getType();
+            if (mFrontendHandle != null && type != mFrontendType) {
+                Log.e(TAG, "Frontend was opened with type " + mFrontendType
+                        + ", new type is " + type);
+                return RESULT_INVALID_STATE;
+            }
+            Log.d(TAG, "Tune to " + settings.getFrequencyLong());
+            mFrontendType = type;
+            if (mFrontendType == FrontendSettings.TYPE_DTMB) {
+                if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
+                        TunerVersionChecker.TUNER_VERSION_1_1, "Tuner with DTMB Frontend")) {
+                    return RESULT_UNAVAILABLE;
+                }
+            }
+
+            if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
+                mFrontendInfo = null;
+                Log.d(TAG, "Write Stats Log for tuning.");
+                FrameworkStatsLog
+                        .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
+                            FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__TUNING);
+                int res = nativeTune(settings.getType(), settings);
+                return res;
+            } else {
                 return RESULT_UNAVAILABLE;
             }
+        } finally {
+            mFrontendLock.unlock();
         }
-        if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) {
-            mFrontendInfo = null;
-            Log.d(TAG, "Write Stats Log for tuning.");
-            FrameworkStatsLog
-                    .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
-                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__TUNING);
-            return nativeTune(settings.getType(), settings);
-        }
-        return RESULT_UNAVAILABLE;
     }
 
     /**
@@ -797,7 +855,12 @@
      */
     @Result
     public int cancelTuning() {
-        return nativeStopTune();
+        mFrontendLock.lock();
+        try {
+            return nativeStopTune();
+        } finally {
+            mFrontendLock.unlock();
+        }
     }
 
     /**
@@ -824,33 +887,41 @@
     @Result
     public int scan(@NonNull FrontendSettings settings, @ScanType int scanType,
             @NonNull @CallbackExecutor Executor executor, @NonNull ScanCallback scanCallback) {
-        synchronized (mScanCallbackLock) {
-            // Scan can be called again for blink scan if scanCallback and executor are same as
-            //before.
-            if (((mScanCallback != null) && (mScanCallback != scanCallback))
-                || ((mScanCallbackExecutor != null) && (mScanCallbackExecutor != executor))) {
-                throw new IllegalStateException(
-                    "Different Scan session already in progress.  stopScan must be called "
-                        + "before a new scan session can be " + "started.");
-            }
-            mFrontendType = settings.getType();
-            if (mFrontendType == FrontendSettings.TYPE_DTMB) {
-                if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
-                        TunerVersionChecker.TUNER_VERSION_1_1,
-                        "Scan with DTMB Frontend")) {
-                    return RESULT_UNAVAILABLE;
+
+        mFrontendLock.lock();
+        try {
+            synchronized (mScanCallbackLock) {
+                // Scan can be called again for blink scan if scanCallback and executor are same as
+                //before.
+                if (((mScanCallback != null) && (mScanCallback != scanCallback))
+                        || ((mScanCallbackExecutor != null)
+                            && (mScanCallbackExecutor != executor))) {
+                    throw new IllegalStateException(
+                        "Different Scan session already in progress.  stopScan must be called "
+                            + "before a new scan session can be " + "started.");
                 }
+                mFrontendType = settings.getType();
+                if (mFrontendType == FrontendSettings.TYPE_DTMB) {
+                    if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
+                            TunerVersionChecker.TUNER_VERSION_1_1,
+                            "Scan with DTMB Frontend")) {
+                        return RESULT_UNAVAILABLE;
+                    }
+                }
+                if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND,
+                          mFrontendLock)) {
+                    mScanCallback = scanCallback;
+                    mScanCallbackExecutor = executor;
+                    mFrontendInfo = null;
+                    FrameworkStatsLog
+                            .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
+                            FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SCANNING);
+                    return nativeScan(settings.getType(), settings, scanType);
+                }
+                return RESULT_UNAVAILABLE;
             }
-            if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) {
-                mScanCallback = scanCallback;
-                mScanCallbackExecutor = executor;
-                mFrontendInfo = null;
-                FrameworkStatsLog
-                    .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
-                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SCANNING);
-                return nativeScan(settings.getType(), settings, scanType);
-            }
-            return RESULT_UNAVAILABLE;
+        } finally {
+            mFrontendLock.unlock();
         }
     }
 
@@ -867,14 +938,19 @@
      */
     @Result
     public int cancelScanning() {
-        synchronized (mScanCallbackLock) {
-            FrameworkStatsLog.write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
-                    FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SCAN_STOPPED);
+        mFrontendLock.lock();
+        try {
+            synchronized (mScanCallbackLock) {
+                FrameworkStatsLog.write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
+                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SCAN_STOPPED);
 
-            int retVal = nativeStopScan();
-            mScanCallback = null;
-            mScanCallbackExecutor = null;
-            return retVal;
+                int retVal = nativeStopScan();
+                mScanCallback = null;
+                mScanCallbackExecutor = null;
+                return retVal;
+            }
+        } finally {
+            mFrontendLock.unlock();
         }
     }
 
@@ -903,7 +979,12 @@
      */
     @Result
     private int setLnb(@NonNull Lnb lnb) {
-        return nativeSetLnb(lnb);
+        mLnbLock.lock();
+        try {
+            return nativeSetLnb(lnb);
+        } finally {
+            mLnbLock.unlock();
+        }
     }
 
     /**
@@ -929,10 +1010,15 @@
      */
     @Nullable
     public FrontendStatus getFrontendStatus(@NonNull @FrontendStatusType int[] statusTypes) {
-        if (mFrontend == null) {
-            throw new IllegalStateException("frontend is not initialized");
+        mFrontendLock.lock();
+        try {
+            if (mFrontend == null) {
+                throw new IllegalStateException("frontend is not initialized");
+            }
+            return nativeGetFrontendStatus(statusTypes);
+        } finally {
+            mFrontendLock.unlock();
         }
-        return nativeGetFrontendStatus(statusTypes);
     }
 
     /**
@@ -942,11 +1028,16 @@
      * @return the id of hardware A/V sync.
      */
     public int getAvSyncHwId(@NonNull Filter filter) {
-        if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
-            return INVALID_AV_SYNC_ID;
+        mDemuxLock.lock();
+        try {
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+                return INVALID_AV_SYNC_ID;
+            }
+            Integer id = nativeGetAvSyncHwId(filter);
+            return id == null ? INVALID_AV_SYNC_ID : id;
+        } finally {
+            mDemuxLock.unlock();
         }
-        Integer id = nativeGetAvSyncHwId(filter);
-        return id == null ? INVALID_AV_SYNC_ID : id;
     }
 
     /**
@@ -959,11 +1050,16 @@
      * @return the current timestamp of hardware A/V sync.
      */
     public long getAvSyncTime(int avSyncHwId) {
-        if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
-            return INVALID_TIMESTAMP;
+        mDemuxLock.lock();
+        try {
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+                return INVALID_TIMESTAMP;
+            }
+            Long time = nativeGetAvSyncTime(avSyncHwId);
+            return time == null ? INVALID_TIMESTAMP : time;
+        } finally {
+            mDemuxLock.unlock();
         }
-        Long time = nativeGetAvSyncTime(avSyncHwId);
-        return time == null ? INVALID_TIMESTAMP : time;
     }
 
     /**
@@ -980,10 +1076,15 @@
      */
     @Result
     public int connectCiCam(int ciCamId) {
-        if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
-            return nativeConnectCiCam(ciCamId);
+        mDemuxLock.lock();
+        try {
+            if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+                return nativeConnectCiCam(ciCamId);
+            }
+            return RESULT_UNAVAILABLE;
+        } finally {
+            mDemuxLock.unlock();
         }
-        return RESULT_UNAVAILABLE;
     }
 
     /**
@@ -1011,14 +1112,30 @@
      *         {@link TunerVersionChecker#getTunerVersion()}.
      */
     public int connectFrontendToCiCam(int ciCamId) {
-        if (TunerVersionChecker.checkHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1,
-                "linkFrontendToCiCam")) {
-            if (checkCiCamResource(ciCamId)
-                    && checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) {
-                return nativeLinkCiCam(ciCamId);
+        // TODO: change this so TRMS lock is held only when the resource handles for
+        // CiCam/Frontend is null. Current implementation can only handle one local lock for that.
+        acquireTRMSLock("connectFrontendToCiCam()");
+        mFrontendCiCamLock.lock();
+        mFrontendLock.lock();
+        try {
+            if (TunerVersionChecker.checkHigherOrEqualVersionTo(
+                    TunerVersionChecker.TUNER_VERSION_1_1,
+                    "linkFrontendToCiCam")) {
+                mRequestedCiCamId = ciCamId;
+                // No need to unlock mFrontendCiCamLock and mFrontendLock below becauase
+                // TRMS lock is already acquired. Pass null to disable lock related operations
+                if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM, null)
+                        && checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, null)
+                    ) {
+                    return nativeLinkCiCam(ciCamId);
+                }
             }
+            return INVALID_LTS_ID;
+        } finally {
+            releaseTRMSLock();
+            mFrontendCiCamLock.unlock();
+            mFrontendLock.unlock();
         }
-        return INVALID_LTS_ID;
     }
 
     /**
@@ -1033,10 +1150,15 @@
      */
     @Result
     public int disconnectCiCam() {
-        if (mDemuxHandle != null) {
-            return nativeDisconnectCiCam();
+        mDemuxLock.lock();
+        try {
+            if (mDemuxHandle != null) {
+                return nativeDisconnectCiCam();
+            }
+            return RESULT_UNAVAILABLE;
+        } finally {
+            mDemuxLock.unlock();
         }
-        return RESULT_UNAVAILABLE;
     }
 
     /**
@@ -1057,20 +1179,30 @@
      */
     @Result
     public int disconnectFrontendToCiCam(int ciCamId) {
-        if (TunerVersionChecker.checkHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1,
-                "unlinkFrontendToCiCam")) {
-            if (mFrontendCiCamHandle != null && mFrontendCiCamId != null
-                    && mFrontendCiCamId == ciCamId) {
-                int result = nativeUnlinkCiCam(ciCamId);
-                if (result == RESULT_SUCCESS) {
-                    mTunerResourceManager.releaseCiCam(mFrontendCiCamHandle, mClientId);
-                    mFrontendCiCamId = null;
-                    mFrontendCiCamHandle = null;
+        acquireTRMSLock("disconnectFrontendToCiCam()");
+        try {
+            if (TunerVersionChecker.checkHigherOrEqualVersionTo(
+                    TunerVersionChecker.TUNER_VERSION_1_1,
+                    "unlinkFrontendToCiCam")) {
+                mFrontendCiCamLock.lock();
+                if (mFrontendCiCamHandle != null && mFrontendCiCamId != null
+                        && mFrontendCiCamId == ciCamId) {
+                    int result = nativeUnlinkCiCam(ciCamId);
+                    if (result == RESULT_SUCCESS) {
+                        mTunerResourceManager.releaseCiCam(mFrontendCiCamHandle, mClientId);
+                        mFrontendCiCamId = null;
+                        mFrontendCiCamHandle = null;
+                    }
+                    return result;
                 }
-                return result;
             }
+            return RESULT_UNAVAILABLE;
+        } finally {
+            if (mFrontendCiCamLock.isLocked()) {
+                mFrontendCiCamLock.unlock();
+            }
+            releaseTRMSLock();
         }
-        return RESULT_UNAVAILABLE;
     }
 
     /**
@@ -1082,16 +1214,21 @@
      */
     @Nullable
     public FrontendInfo getFrontendInfo() {
-        if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) {
-            return null;
+        mFrontendLock.lock();
+        try {
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
+                return null;
+            }
+            if (mFrontend == null) {
+                throw new IllegalStateException("frontend is not initialized");
+            }
+            if (mFrontendInfo == null) {
+                mFrontendInfo = getFrontendInfoById(mFrontend.mId);
+            }
+            return mFrontendInfo;
+        } finally {
+            mFrontendLock.unlock();
         }
-        if (mFrontend == null) {
-            throw new IllegalStateException("frontend is not initialized");
-        }
-        if (mFrontendInfo == null) {
-            mFrontendInfo = getFrontendInfoById(mFrontend.mId);
-        }
-        return mFrontendInfo;
     }
 
     /**
@@ -1114,7 +1251,12 @@
 
     /** @hide */
     public FrontendInfo getFrontendInfoById(int id) {
-        return nativeGetFrontendInfo(id);
+        mFrontendLock.lock();
+        try {
+            return nativeGetFrontendInfo(id);
+        } finally {
+            mFrontendLock.unlock();
+        }
     }
 
     /**
@@ -1125,7 +1267,12 @@
      */
     @Nullable
     public DemuxCapabilities getDemuxCapabilities() {
-        return nativeGetDemuxCapabilities();
+        mDemuxLock.lock();
+        try {
+            return nativeGetDemuxCapabilities();
+        } finally {
+            mDemuxLock.unlock();
+        }
     }
 
     private void onFrontendEvent(int eventType) {
@@ -1417,32 +1564,37 @@
     public Filter openFilter(@Type int mainType, @Subtype int subType,
             @BytesLong long bufferSize, @CallbackExecutor @Nullable Executor executor,
             @Nullable FilterCallback cb) {
-        if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
-            return null;
-        }
-        Filter filter = nativeOpenFilter(
-                mainType, TunerUtils.getFilterSubtype(mainType, subType), bufferSize);
-        if (filter != null) {
-            filter.setType(mainType, subType);
-            filter.setCallback(cb, executor);
-            if (mHandler == null) {
-                mHandler = createEventHandler();
+        mDemuxLock.lock();
+        try {
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+                return null;
             }
-            synchronized (mFilters) {
-                WeakReference<Filter> weakFilter = new WeakReference<Filter>(filter);
-                mFilters.add(weakFilter);
-                if (mFilters.size() > FILTER_CLEANUP_THRESHOLD) {
-                    Iterator<WeakReference<Filter>> iterator = mFilters.iterator();
-                    while (iterator.hasNext()) {
-                        WeakReference<Filter> wFilter = iterator.next();
-                        if (wFilter.get() == null) {
-                            iterator.remove();
+            Filter filter = nativeOpenFilter(
+                    mainType, TunerUtils.getFilterSubtype(mainType, subType), bufferSize);
+            if (filter != null) {
+                filter.setType(mainType, subType);
+                filter.setCallback(cb, executor);
+                if (mHandler == null) {
+                    mHandler = createEventHandler();
+                }
+                synchronized (mFilters) {
+                    WeakReference<Filter> weakFilter = new WeakReference<Filter>(filter);
+                    mFilters.add(weakFilter);
+                    if (mFilters.size() > FILTER_CLEANUP_THRESHOLD) {
+                        Iterator<WeakReference<Filter>> iterator = mFilters.iterator();
+                        while (iterator.hasNext()) {
+                            WeakReference<Filter> wFilter = iterator.next();
+                            if (wFilter.get() == null) {
+                                iterator.remove();
+                            }
                         }
                     }
                 }
             }
+            return filter;
+        } finally {
+            mDemuxLock.unlock();
         }
-        return filter;
     }
 
     /**
@@ -1457,18 +1609,24 @@
      */
     @Nullable
     public Lnb openLnb(@CallbackExecutor @NonNull Executor executor, @NonNull LnbCallback cb) {
-        Objects.requireNonNull(executor, "executor must not be null");
-        Objects.requireNonNull(cb, "LnbCallback must not be null");
-        if (mLnb != null) {
-            mLnb.setCallback(executor, cb, this);
-            return mLnb;
+        mLnbLock.lock();
+        try {
+            Objects.requireNonNull(executor, "executor must not be null");
+            Objects.requireNonNull(cb, "LnbCallback must not be null");
+            if (mLnb != null) {
+                mLnb.setCallback(executor, cb, this);
+                return mLnb;
+            }
+            if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_LNB, mLnbLock)
+                    && mLnb != null) {
+                mLnb.setCallback(executor, cb, this);
+                setLnb(mLnb);
+                return mLnb;
+            }
+            return null;
+        } finally {
+            mLnbLock.unlock();
         }
-        if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_LNB) && mLnb != null) {
-            mLnb.setCallback(executor, cb, this);
-            setLnb(mLnb);
-            return mLnb;
-        }
-        return null;
     }
 
     /**
@@ -1483,20 +1641,25 @@
     @Nullable
     public Lnb openLnbByName(@NonNull String name, @CallbackExecutor @NonNull Executor executor,
             @NonNull LnbCallback cb) {
-        Objects.requireNonNull(name, "LNB name must not be null");
-        Objects.requireNonNull(executor, "executor must not be null");
-        Objects.requireNonNull(cb, "LnbCallback must not be null");
-        Lnb newLnb = nativeOpenLnbByName(name);
-        if (newLnb != null) {
-            if (mLnb != null) {
-                mLnb.close();
-                mLnbHandle = null;
+        mLnbLock.lock();
+        try {
+            Objects.requireNonNull(name, "LNB name must not be null");
+            Objects.requireNonNull(executor, "executor must not be null");
+            Objects.requireNonNull(cb, "LnbCallback must not be null");
+            Lnb newLnb = nativeOpenLnbByName(name);
+            if (newLnb != null) {
+                if (mLnb != null) {
+                    mLnb.close();
+                    mLnbHandle = null;
+                }
+                mLnb = newLnb;
+                mLnb.setCallback(executor, cb, this);
+                setLnb(mLnb);
             }
-            mLnb = newLnb;
-            mLnb.setCallback(executor, cb, this);
-            setLnb(mLnb);
+            return mLnb;
+        } finally {
+            mLnbLock.unlock();
         }
-        return mLnb;
     }
 
     private boolean requestLnb() {
@@ -1518,10 +1681,15 @@
      */
     @Nullable
     public TimeFilter openTimeFilter() {
-        if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
-            return null;
+        mDemuxLock.lock();
+        try {
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+                return null;
+            }
+            return nativeOpenTimeFilter();
+        } finally {
+            mDemuxLock.unlock();
         }
-        return nativeOpenTimeFilter();
     }
 
     /**
@@ -1532,10 +1700,15 @@
     @RequiresPermission(android.Manifest.permission.ACCESS_TV_DESCRAMBLER)
     @Nullable
     public Descrambler openDescrambler() {
-        if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
-            return null;
+        mDemuxLock.lock();
+        try {
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+                return null;
+            }
+            return requestDescrambler();
+        } finally {
+            mDemuxLock.unlock();
         }
-        return requestDescrambler();
     }
 
     /**
@@ -1553,14 +1726,19 @@
             @BytesLong long bufferSize,
             @CallbackExecutor @NonNull Executor executor,
             @NonNull OnRecordStatusChangedListener l) {
-        Objects.requireNonNull(executor, "executor must not be null");
-        Objects.requireNonNull(l, "OnRecordStatusChangedListener must not be null");
-        if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
-            return null;
+        mDemuxLock.lock();
+        try {
+            Objects.requireNonNull(executor, "executor must not be null");
+            Objects.requireNonNull(l, "OnRecordStatusChangedListener must not be null");
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+                return null;
+            }
+            DvrRecorder dvr = nativeOpenDvrRecorder(bufferSize);
+            dvr.setListener(executor, l);
+            return dvr;
+        } finally {
+            mDemuxLock.unlock();
         }
-        DvrRecorder dvr = nativeOpenDvrRecorder(bufferSize);
-        dvr.setListener(executor, l);
-        return dvr;
     }
 
     /**
@@ -1578,14 +1756,19 @@
             @BytesLong long bufferSize,
             @CallbackExecutor @NonNull Executor executor,
             @NonNull OnPlaybackStatusChangedListener l) {
-        Objects.requireNonNull(executor, "executor must not be null");
-        Objects.requireNonNull(l, "OnPlaybackStatusChangedListener must not be null");
-        if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) {
-            return null;
+        mDemuxLock.lock();
+        try {
+            Objects.requireNonNull(executor, "executor must not be null");
+            Objects.requireNonNull(l, "OnPlaybackStatusChangedListener must not be null");
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+                return null;
+            }
+            DvrPlayback dvr = nativeOpenDvrPlayback(bufferSize);
+            dvr.setListener(executor, l);
+            return dvr;
+        } finally {
+            mDemuxLock.unlock();
         }
-        DvrPlayback dvr = nativeOpenDvrPlayback(bufferSize);
-        dvr.setListener(executor, l);
-        return dvr;
     }
 
     /**
@@ -1602,6 +1785,8 @@
     static public SharedFilter openSharedFilter(@NonNull Context context,
             @NonNull String sharedFilterToken, @CallbackExecutor @NonNull Executor executor,
             @NonNull SharedFilterCallback cb) {
+        // TODO: check what happenes when onReclaimResources() is called and see if
+        // this needs to be protected with TRMS lock
         Objects.requireNonNull(sharedFilterToken, "sharedFilterToken must not be null");
         Objects.requireNonNull(executor, "executor must not be null");
         Objects.requireNonNull(cb, "SharedFilterCallback must not be null");
@@ -1665,22 +1850,28 @@
         return granted;
     }
 
-    private boolean checkResource(int resourceType)  {
+    private boolean checkResource(int resourceType, ReentrantLock localLock)  {
         switch (resourceType) {
             case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND: {
-                if (mFrontendHandle == null && !requestFrontend()) {
+                if (mFrontendHandle == null && !requestResource(resourceType, localLock)) {
                     return false;
                 }
                 break;
             }
             case TunerResourceManager.TUNER_RESOURCE_TYPE_LNB: {
-                if (mLnb == null && !requestLnb()) {
+                if (mLnb == null && !requestResource(resourceType, localLock)) {
                     return false;
                 }
                 break;
             }
             case TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX: {
-                if (mDemuxHandle == null && !requestDemux()) {
+                if (mDemuxHandle == null && !requestResource(resourceType, localLock)) {
+                    return false;
+                }
+                break;
+            }
+            case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM: {
+                if (mFrontendCiCamHandle == null && !requestResource(resourceType, localLock)) {
                     return false;
                 }
                 break;
@@ -1691,24 +1882,91 @@
         return true;
     }
 
-    private boolean checkCiCamResource(int ciCamId) {
-        if (mFrontendCiCamHandle == null && !requestFrontendCiCam(ciCamId)) {
-            return false;
+    // Expected flow of how to use this function is:
+    // 1) lock the localLock and check if the resource is already held
+    // 2) if yes, no need to call this function and continue with the handle with the lock held
+    // 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)  {
+        boolean enableLockOperations = localLock != null;
+
+        // release the local lock first to avoid deadlock
+        if (enableLockOperations) {
+            if (localLock.isLocked()) {
+                localLock.unlock();
+            } else {
+                throw new IllegalStateException("local lock must be locked beforehand");
+            }
         }
-        return true;
+
+        // now safe to grab TRMS lock
+        if (enableLockOperations) {
+            acquireTRMSLock("requestResource:" + resourceType);
+        }
+
+        try {
+            // lock the local lock
+            if (enableLockOperations) {
+                localLock.lock();
+            }
+            switch (resourceType) {
+                case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND: {
+                    return requestFrontend();
+                }
+                case TunerResourceManager.TUNER_RESOURCE_TYPE_LNB: {
+                    return requestLnb();
+                }
+                case TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX: {
+                    return requestDemux();
+                }
+                case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM: {
+                    return requestFrontendCiCam(mRequestedCiCamId);
+                }
+                default:
+                    return false;
+            }
+        } finally {
+            if (enableLockOperations) {
+                releaseTRMSLock();
+            }
+        }
     }
 
     /* package */ void releaseLnb() {
-        if (mLnbHandle != null) {
-            // LNB handle can be null if it's opened by name.
-            mTunerResourceManager.releaseLnb(mLnbHandle, mClientId);
-            mLnbHandle = null;
+        acquireTRMSLock("releaseLnb()");
+        mLnbLock.lock();
+        try {
+            if (mLnbHandle != null) {
+                // LNB handle can be null if it's opened by name.
+                mTunerResourceManager.releaseLnb(mLnbHandle, mClientId);
+                mLnbHandle = null;
+            }
+            mLnb = null;
+        } finally {
+            releaseTRMSLock();
+            mLnbLock.unlock();
         }
-        mLnb = null;
     }
 
     /** @hide */
     public int getClientId() {
         return mClientId;
     }
+
+    private void acquireTRMSLock(String functionNameForLog) {
+        if (DEBUG) {
+            Log.d(TAG, "ATTEMPT:acquireLock() in " + functionNameForLog
+                    + "for clientId:" + mClientId);
+        }
+        if (!mTunerResourceManager.acquireLock(mClientId)) {
+            Log.e(TAG, "FAILED:acquireLock() in " + functionNameForLog
+                    + " for clientId:" + mClientId + " - this can cause deadlock between"
+                    + " Tuner API calls and onReclaimResources()");
+        }
+    }
+
+    private void releaseTRMSLock() {
+        mTunerResourceManager.releaseLock(mClientId);
+    }
 }
diff --git a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
index 244fd0e..fe611c7 100644
--- a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
+++ b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
@@ -320,6 +320,48 @@
     }
 
     /**
+     * Grants the lock to the caller for public {@link Tuner} APIs
+     *
+     * <p>{@link Tuner} functions that call both [@link TunerResourceManager} APIs and
+     * grabs lock that are also used in {@link IResourcesReclaimListener#onReclaimResources()}
+     * must call this API before acquiring lock used in onReclaimResources().
+     *
+     * <p>This API will block until it releases the lock or fails
+     *
+     * @param clientId The ID of the caller.
+     *
+     * @return true if the lock is granted. If false is returned, calling this API again is not
+     * guaranteed to work and may be unrecoverrable. (This should not happen.)
+     */
+    public boolean acquireLock(int clientId) {
+        try {
+            return mService.acquireLock(clientId, Thread.currentThread().getId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Releases the lock to the caller for public {@link Tuner} APIs
+     *
+     * <p>This API must be called in pair with {@link #acquireLock(int, int)}
+     *
+     * <p>This API will block until it releases the lock or fails
+     *
+     * @param clientId The ID of the caller.
+     *
+     * @return true if the lock is granted. If false is returned, calling this API again is not
+     * guaranteed to work and may be unrecoverrable. (This should not happen.)
+     */
+    public boolean releaseLock(int clientId) {
+        try {
+            return mService.releaseLock(clientId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Requests a frontend resource.
      *
      * <p>There are three possible scenarios:
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 7bc5058..5f35820 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
@@ -412,4 +412,34 @@
      * @param resourceType The resource type to restore the map for.
      */
     void restoreResourceMap(in int resourceType);
+
+    /**
+     * Grants the lock to the caller for public {@link Tuner} APIs
+     *
+     * <p>{@link Tuner} functions that call both [@link TunerResourceManager} APIs and
+     * grabs lock that are also used in {@link IResourcesReclaimListener#onReclaimResources()}
+     * must call this API before acquiring lock used in onReclaimResources().
+     *
+     * <p>This API will block until it releases the lock or fails
+     *
+     * @param clientId The ID of the caller.
+     *
+     * @return true if the lock is granted. If false is returned, calling this API again is not
+     * guaranteed to work and may be unrecoverrable. (This should not happen.)
+     */
+    boolean acquireLock(in int clientId, in long clientThreadId);
+
+    /**
+     * Releases the lock to the caller for public {@link Tuner} APIs
+     *
+     * <p>This API must be called in pair with {@link #acquireLock(int, int)}
+     *
+     * <p>This API will block until it releases the lock or fails
+     *
+     * @param clientId The ID of the caller.
+     *
+     * @return true if the lock is granted. If false is returned, calling this API again is not
+     * guaranteed to work and may be unrecoverrable. (This should not happen.)
+     */
+    boolean releaseLock(in int clientId);
 }
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml
index 9d3ce34..cc7bb4a 100644
--- a/packages/EasterEgg/AndroidManifest.xml
+++ b/packages/EasterEgg/AndroidManifest.xml
@@ -15,6 +15,8 @@
     <!-- controls -->
     <uses-permission android:name="android.permission.BIND_CONTROLS" />
 
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
     <application
         android:icon="@drawable/icon"
         android:label="@string/app_name">
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index 389892e..8ac4e38 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -170,6 +170,12 @@
     }
 
     @VisibleForTesting
+    void registerIntentReceiver() {
+        mContext.registerReceiverAsUser(mBroadcastReceiver, mUserHandle, mAdapterIntentFilter,
+                null, mReceiverHandler);
+    }
+
+    @VisibleForTesting
     void addProfileHandler(String action, Handler handler) {
         mHandlerMap.put(action, handler);
         mProfileIntentFilter.addAction(action);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
index 4bff78f..bee466d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
@@ -109,7 +109,7 @@
                         /* handler= */ null, /* userHandle= */ null);
 
         verify(mockContext).registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class),
-                eq(null), eq(null));
+                eq(null), eq(null), eq(Context.RECEIVER_EXPORTED));
     }
 
     @Test
@@ -120,7 +120,7 @@
                         /* handler= */ null, UserHandle.ALL);
 
         verify(mockContext).registerReceiverAsUser(any(BroadcastReceiver.class), eq(UserHandle.ALL),
-                any(IntentFilter.class), eq(null), eq(null));
+                any(IntentFilter.class), eq(null), eq(null), eq(Context.RECEIVER_EXPORTED));
     }
 
     /**
@@ -129,6 +129,7 @@
     @Test
     public void intentWithExtraState_audioStateChangedShouldDispatchToRegisterCallback() {
         mBluetoothEventManager.registerCallback(mBluetoothCallback);
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
 
         mContext.sendBroadcast(mIntent);
@@ -142,6 +143,7 @@
     @Test
     public void intentWithExtraState_phoneStateChangedShouldDispatchToRegisterCallback() {
         mBluetoothEventManager.registerCallback(mBluetoothCallback);
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
 
         mContext.sendBroadcast(mIntent);
@@ -167,6 +169,7 @@
     @Test
     public void dispatchAclConnectionStateChanged_aclDisconnected_shouldDispatchCallback() {
         mBluetoothEventManager.registerCallback(mBluetoothCallback);
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
 
@@ -179,6 +182,7 @@
     @Test
     public void dispatchAclConnectionStateChanged_aclConnected_shouldDispatchCallback() {
         mBluetoothEventManager.registerCallback(mBluetoothCallback);
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
 
@@ -192,6 +196,7 @@
     public void dispatchAclConnectionStateChanged_aclDisconnected_shouldNotCallbackSubDevice() {
         when(mCachedDeviceManager.isSubDevice(mBluetoothDevice)).thenReturn(true);
         mBluetoothEventManager.registerCallback(mBluetoothCallback);
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
 
@@ -205,6 +210,7 @@
     public void dispatchAclConnectionStateChanged_aclConnected_shouldNotCallbackSubDevice() {
         when(mCachedDeviceManager.isSubDevice(mBluetoothDevice)).thenReturn(true);
         mBluetoothEventManager.registerCallback(mBluetoothCallback);
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
 
@@ -218,6 +224,7 @@
     public void dispatchAclConnectionStateChanged_findDeviceReturnNull_shouldNotDispatchCallback() {
         when(mCachedDeviceManager.findDevice(mBluetoothDevice)).thenReturn(null);
         mBluetoothEventManager.registerCallback(mBluetoothCallback);
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
 
@@ -354,6 +361,7 @@
 
     @Test
     public void showUnbondMessage_reasonAuthTimeout_showCorrectedErrorCode() {
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
         mIntent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
@@ -369,6 +377,7 @@
 
     @Test
     public void showUnbondMessage_reasonRemoteDeviceDown_showCorrectedErrorCode() {
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
         mIntent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
@@ -385,6 +394,7 @@
 
     @Test
     public void showUnbondMessage_reasonAuthRejected_showCorrectedErrorCode() {
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
         mIntent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
@@ -400,6 +410,7 @@
 
     @Test
     public void showUnbondMessage_reasonAuthFailed_showCorrectedErrorCode() {
+        mBluetoothEventManager.registerIntentReceiver();
         mIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
         mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
         mIntent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 892cd63..1e2d4e9 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -25,6 +25,7 @@
 import static android.provider.Settings.SET_ALL_RESULT_DISABLED;
 import static android.provider.Settings.SET_ALL_RESULT_FAILURE;
 import static android.provider.Settings.SET_ALL_RESULT_SUCCESS;
+import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_MAGNIFICATION_CONTROLLER;
 import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON_OVERLAY;
@@ -3623,7 +3624,7 @@
         }
 
         private final class UpgradeController {
-            private static final int SETTINGS_VERSION = 207;
+            private static final int SETTINGS_VERSION = 208;
 
             private final int mUserId;
 
@@ -5472,6 +5473,30 @@
                     currentVersion = 207;
                 }
 
+                if (currentVersion == 207) {
+                    // Version 207: Reset the
+                    // Secure#ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT as enabled
+                    // status for showing the tooltips.
+                    final SettingsState secureSettings = getSecureSettingsLocked(userId);
+                    final Setting accessibilityButtonMode = secureSettings.getSettingLocked(
+                            Secure.ACCESSIBILITY_BUTTON_MODE);
+                    if (!accessibilityButtonMode.isNull()
+                            && accessibilityButtonMode.getValue().equals(
+                            String.valueOf(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU))) {
+                        if (isGestureNavigateEnabled()
+                                && hasValueInA11yButtonTargets(secureSettings)) {
+                            secureSettings.insertSettingLocked(
+                                    Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
+                                    /* enabled */ "1",
+                                    /* tag= */ null,
+                                    /* makeDefault= */ false,
+                                    SettingsState.SYSTEM_PACKAGE_NAME);
+                        }
+                    }
+
+                    currentVersion = 208;
+                }
+
                 // vXXX: Add new settings above this point.
 
                 if (currentVersion != newVersion) {
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 36b633b..262cf53 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -603,6 +603,9 @@
     <!-- Permission required for ATS test - CarDevicePolicyManagerTest -->
     <uses-permission android:name="android.permission.LOCK_DEVICE" />
 
+    <!-- Permission required for CTS test - CtsSafetyCenterTestCases -->
+    <uses-permission android:name="android.permission.SEND_SAFETY_CENTER_UPDATE" />
+
     <application android:label="@string/app_label"
                 android:theme="@android:style/Theme.DeviceDefault.DayNight"
                 android:defaultToDeviceProtectedStorage="true"
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index 4fc38a8..1601043 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -219,6 +219,9 @@
     <!-- Face hint message when finger was not recognized. [CHAR LIMIT=20] -->
     <string name="kg_face_not_recognized">Not recognized</string>
 
+     <!-- Error message indicating that the camera privacy sensor has been turned on [CHAR LIMIT=NONE] -->
+    <string name="kg_face_sensor_privacy_enabled">To use Face Unlock, turn on <b>Camera access</b> in Settings > Privacy</string>
+
     <!-- Instructions telling the user remaining times when enter SIM PIN view.  -->
     <plurals name="kg_password_default_pin_message">
         <item quantity="one">Enter SIM PIN. You have <xliff:g id="number">%d</xliff:g> remaining
diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml
index 1e0ce00..46cbc25 100644
--- a/packages/SystemUI/res/layout/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml
@@ -18,64 +18,71 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     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:orientation="vertical">
 
-    <Space
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_weight="1"/>
-
-    <ImageView
-        android:id="@+id/icon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
-
-    <TextView
-        android:id="@+id/title"
+    <RelativeLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        style="@style/TextAppearance.AuthCredential.Title"/>
+        android:layout_height="match_parent">
 
-    <TextView
-        android:id="@+id/subtitle"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        style="@style/TextAppearance.AuthCredential.Subtitle"/>
+        <LinearLayout
+            android:id="@+id/auth_credential_header"
+            style="@style/AuthCredentialHeaderStyle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentTop="true">
 
-    <TextView
-        android:id="@+id/description"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        style="@style/TextAppearance.AuthCredential.Description"/>
+            <ImageView
+                android:id="@+id/icon"
+                android:layout_width="48dp"
+                android:layout_height="48dp"
+                android:contentDescription="@null" />
 
-    <Space
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_weight="1"/>
+            <TextView
+                android:id="@+id/title"
+                style="@style/TextAppearance.AuthCredential.Title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
 
-    <ImeAwareEditText
-        android:id="@+id/lockPassword"
-        android:layout_width="208dp"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_horizontal"
-        android:minHeight="48dp"
-        android:gravity="center"
-        android:inputType="textPassword"
-        android:maxLength="500"
-        android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii"
-        style="@style/TextAppearance.AuthCredential.PasswordEntry"/>
+            <TextView
+                android:id="@+id/subtitle"
+                style="@style/TextAppearance.AuthCredential.Subtitle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
 
-    <TextView
-        android:id="@+id/error"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        style="@style/TextAppearance.AuthCredential.Error"/>
+            <TextView
+                android:id="@+id/description"
+                style="@style/TextAppearance.AuthCredential.Description"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
 
-    <Space
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_weight="5"/>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:layout_alignParentBottom="true">
+
+            <ImeAwareEditText
+                android:id="@+id/lockPassword"
+                style="@style/TextAppearance.AuthCredential.PasswordEntry"
+                android:layout_width="208dp"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii"
+                android:inputType="textPassword"
+                android:minHeight="48dp" />
+
+            <TextView
+                android:id="@+id/error"
+                style="@style/TextAppearance.AuthCredential.Error"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+        </LinearLayout>
+
+    </RelativeLayout>
 
 </com.android.systemui.biometrics.AuthCredentialPasswordView>
\ 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 4939ea2..470298e 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -22,76 +22,81 @@
     android:gravity="center_horizontal"
     android:elevation="@dimen/biometric_dialog_elevation">
 
-    <Space
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_weight="1"/>
-
-    <ImageView
-        android:id="@+id/icon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
-
-    <TextView
-        android:id="@+id/title"
+    <RelativeLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        style="@style/TextAppearance.AuthCredential.Title"/>
+        android:layout_height="match_parent"
+        android:orientation="vertical">
 
-    <TextView
-        android:id="@+id/subtitle"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        style="@style/TextAppearance.AuthCredential.Subtitle"/>
+        <LinearLayout
+            android:id="@+id/auth_credential_header"
+            style="@style/AuthCredentialHeaderStyle"
+            android:layout_width="match_parent"
+            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"/>
+            <ImageView
+                android:id="@+id/icon"
+                android:layout_width="48dp"
+                android:layout_height="48dp"
+                android:contentDescription="@null" />
 
-    <Space
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_weight="1"/>
+            <TextView
+                android:id="@+id/title"
+                style="@style/TextAppearance.AuthCredential.Title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
 
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        android:gravity="center"
-        android:paddingLeft="0dp"
-        android:paddingRight="0dp"
-        android:paddingTop="0dp"
-        android:paddingBottom="16dp"
-        android:clipToPadding="false">
+            <TextView
+                android:id="@+id/subtitle"
+                style="@style/TextAppearance.AuthCredential.Subtitle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
 
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="0dp"
-            android:layout_weight="1"
-            style="@style/LockPatternContainerStyle">
+            <TextView
+                android:id="@+id/description"
+                style="@style/TextAppearance.AuthCredential.Description"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+        </LinearLayout>
 
-            <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>
-
-        <TextView
-            android:id="@+id/error"
+        <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Error"/>
+            android:layout_below="@id/auth_credential_header"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:paddingBottom="16dp"
+            android:paddingTop="60dp">
 
-    </LinearLayout>
+            <FrameLayout
+                style="@style/LockPatternContainerStyle"
+                android:layout_width="wrap_content"
+                android:layout_height="0dp"
+                android:layout_weight="1">
 
-    <Space
-        android:layout_width="0dp"
-        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.AuthCredential.Error"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+        </LinearLayout>
+
+    </RelativeLayout>
 
 </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index e90a644..c575855 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -67,7 +67,6 @@
 
         <ProgressBar
             android:id="@+id/wifi_searching_progress"
-            android:indeterminate="true"
             android:layout_width="340dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center_horizontal"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9a85a70..99508a5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -634,7 +634,7 @@
     <!-- QuickSettings: Brightness dialog title [CHAR LIMIT=NONE] -->
     <string name="quick_settings_brightness_dialog_title">Brightness</string>
     <!-- QuickSettings: Label for the toggle that controls whether display inversion is enabled. [CHAR LIMIT=NONE] -->
-    <string name="quick_settings_inversion_label">Invert colors</string>
+    <string name="quick_settings_inversion_label">Color inversion</string>
     <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] -->
     <!-- QuickSettings: Control panel: Label for button that navigates to settings. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_more_settings">More settings</string>
@@ -2050,8 +2050,8 @@
     <string name="magnification_mode_switch_click_label">Switch</string>
 
     <!-- Accessibility floating menu strings -->
-    <!-- Message for the accessibility floating button migration tooltip. It shows when the user use gestural navigation then upgrade their system. It will tell the user the accessibility gesture had been replaced by accessibility floating button. [CHAR LIMIT=100] -->
-    <string name="accessibility_floating_button_migration_tooltip">Accessibility button replaced the accessibility gesture\n\n<annotation id="link">View settings</annotation></string>
+    <!-- Message for the accessibility floating button migration tooltip. It shows when the user use gestural navigation then upgrade their system. It will tell the user they could customize or replace the floating button in Settings. [CHAR LIMIT=100] -->
+    <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>
     <!-- Action in accessibility menu to move the accessibility floating button to the top left of the screen. [CHAR LIMIT=30] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 1a4f3e2..69f9966 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -200,9 +200,9 @@
 
     <style name="TextAppearance.DeviceManagementDialog.Title" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
 
-    <style name="TextAppearance.AuthCredential">
+    <style name="TextAppearance.AuthCredential"
+        parent="@android:style/TextAppearance.DeviceDefault">
         <item name="android:accessibilityLiveRegion">polite</item>
-        <item name="android:gravity">center_horizontal</item>
         <item name="android:textAlignment">gravity</item>
         <item name="android:layout_gravity">top</item>
         <item name="android:textColor">?android:attr/textColorPrimary</item>
@@ -210,44 +210,57 @@
 
     <style name="TextAppearance.AuthCredential.Title">
         <item name="android:fontFamily">google-sans</item>
-        <item name="android:paddingTop">12dp</item>
-        <item name="android:paddingHorizontal">24dp</item>
-        <item name="android:textSize">24sp</item>
+        <item name="android:layout_marginTop">20dp</item>
+        <item name="android:textSize">36sp</item>
     </style>
 
     <style name="TextAppearance.AuthCredential.Subtitle">
         <item name="android:fontFamily">google-sans</item>
-        <item name="android:paddingTop">8dp</item>
-        <item name="android:paddingHorizontal">24dp</item>
-        <item name="android:textSize">16sp</item>
+        <item name="android:layout_marginTop">20dp</item>
+        <item name="android:textSize">18sp</item>
     </style>
 
     <style name="TextAppearance.AuthCredential.Description">
         <item name="android:fontFamily">google-sans</item>
-        <item name="android:paddingTop">8dp</item>
-        <item name="android:paddingHorizontal">24dp</item>
-        <item name="android:textSize">14sp</item>
+        <item name="android:layout_marginTop">20dp</item>
+        <item name="android:textSize">16sp</item>
     </style>
 
     <style name="TextAppearance.AuthCredential.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>
+        <item name="android:gravity">center</item>
     </style>
 
-    <style name="TextAppearance.AuthCredential.PasswordEntry" parent="@android:style/TextAppearance.DeviceDefault">
+    <style name="TextAppearance.AuthCredential.PasswordEntry">
         <item name="android:gravity">center</item>
         <item name="android:singleLine">true</item>
         <item name="android:textColor">?android:attr/colorForeground</item>
         <item name="android:textSize">24sp</item>
     </style>
 
+    <style name="AuthCredentialHeaderStyle">
+        <item name="android:paddingStart">48dp</item>
+        <item name="android:paddingEnd">24dp</item>
+        <item name="android:paddingTop">28dp</item>
+        <item name="android:paddingBottom">20dp</item>
+        <item name="android:orientation">vertical</item>
+        <item name="android:layout_gravity">top</item>
+    </style>
+
     <style name="DeviceManagementDialogTitle">
         <item name="android:gravity">center</item>
         <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item>
     </style>
 
+    <style name="AuthCredentialPasswordTheme" parent="@style/Theme.MaterialComponents.DayNight">
+        <item name="colorPrimary">?android:attr/colorPrimary</item>
+        <item name="colorPrimaryDark">?android:attr/colorPrimary</item>
+    </style>
+
     <style name="TextAppearance.DeviceManagementDialog.Content" parent="@*android:style/TextAppearance.DeviceDefault.Subhead"/>
 
     <style name="BaseBrightnessDialogContainer" parent="@style/Theme.SystemUI">
@@ -307,9 +320,8 @@
         <item name="android:maxWidth">420dp</item>
         <item name="android:minHeight">0dp</item>
         <item name="android:minWidth">0dp</item>
-        <item name="android:paddingBottom">0dp</item>
-        <item name="android:paddingHorizontal">44dp</item>
-        <item name="android:paddingTop">0dp</item>
+        <item name="android:paddingHorizontal">60dp</item>
+        <item name="android:paddingBottom">40dp</item>
     </style>
 
     <style name="LockPatternStyle">
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index ba67716..237ca71 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -51,6 +51,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
@@ -335,6 +336,8 @@
     private boolean mLockIconPressed;
     private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     private final Executor mBackgroundExecutor;
+    private SensorPrivacyManager mSensorPrivacyManager;
+    private int mFaceAuthUserId;
 
     /**
      * Short delay before restarting fingerprint authentication after a successful try. This should
@@ -1016,6 +1019,12 @@
 
         // Error is always the end of authentication lifecycle
         mFaceCancelSignal = null;
+        boolean cameraPrivacyEnabled = false;
+        if (mSensorPrivacyManager != null) {
+            cameraPrivacyEnabled = mSensorPrivacyManager
+                    .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA,
+                    mFaceAuthUserId);
+        }
 
         if (msgId == FaceManager.FACE_ERROR_CANCELED
                 && mFaceRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING) {
@@ -1025,7 +1034,9 @@
             setFaceRunningState(BIOMETRIC_STATE_STOPPED);
         }
 
-        if (msgId == FaceManager.FACE_ERROR_HW_UNAVAILABLE
+        final boolean isHwUnavailable = msgId == FaceManager.FACE_ERROR_HW_UNAVAILABLE;
+
+        if (isHwUnavailable
                 || msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS) {
             if (mHardwareFaceUnavailableRetryCount < HAL_ERROR_RETRY_MAX) {
                 mHardwareFaceUnavailableRetryCount++;
@@ -1041,6 +1052,10 @@
             requireStrongAuthIfAllLockedOut();
         }
 
+        if (isHwUnavailable && cameraPrivacyEnabled) {
+            errString = mContext.getString(R.string.kg_face_sensor_privacy_enabled);
+        }
+
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -1816,6 +1831,7 @@
         mLockPatternUtils = lockPatternUtils;
         mAuthController = authController;
         dumpManager.registerDumpable(getClass().getName(), this);
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
 
         mHandler = new Handler(mainLooper) {
             @Override
@@ -2517,6 +2533,7 @@
             // This would need to be updated for multi-sensor devices
             final boolean supportsFaceDetection = !mFaceSensorProperties.isEmpty()
                     && mFaceSensorProperties.get(0).supportsFaceDetection;
+            mFaceAuthUserId = userId;
             if (isEncryptedOrLockdown(userId) && supportsFaceDetection) {
                 mFaceManager.detectFace(mFaceCancelSignal, mFaceDetectionCallback, userId);
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 29e5574..6edf2a8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -31,6 +31,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.PointF;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager.Authenticators;
@@ -89,6 +90,7 @@
 
     private static final String TAG = "AuthController";
     private static final boolean DEBUG = true;
+    private static final int SENSOR_PRIVACY_DELAY = 500;
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final CommandQueue mCommandQueue;
@@ -122,6 +124,7 @@
     @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
 
     @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
+    private SensorPrivacyManager mSensorPrivacyManager;
 
     private class BiometricTaskStackListener extends TaskStackListener {
         @Override
@@ -492,6 +495,7 @@
         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
 
         context.registerReceiver(mBroadcastReceiver, filter);
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
     }
 
     private void updateFingerprintLocation() {
@@ -642,10 +646,16 @@
         final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT)
                 || (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT);
 
+        boolean isCameraPrivacyEnabled = false;
+        if (error == BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE
+                && mSensorPrivacyManager.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA,
+                mCurrentDialogArgs.argi1 /* userId */)) {
+            isCameraPrivacyEnabled = true;
+        }
         // TODO(b/141025588): Create separate methods for handling hard and soft errors.
         final boolean isSoftError = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED
-                || error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT);
-
+                || error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT
+                || isCameraPrivacyEnabled);
         if (mCurrentDialog != null) {
             if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) {
                 if (DEBUG) Log.d(TAG, "onBiometricError, lockout");
@@ -655,12 +665,23 @@
                         ? mContext.getString(R.string.biometric_not_recognized)
                         : getErrorString(modality, error, vendorCode);
                 if (DEBUG) Log.d(TAG, "onBiometricError, soft error: " + errorMessage);
-                mCurrentDialog.onAuthenticationFailed(modality, errorMessage);
+                // The camera privacy error can return before the prompt initializes its state,
+                // causing the prompt to appear to endlessly authenticate. Add a small delay
+                // to stop this.
+                if (isCameraPrivacyEnabled) {
+                    mHandler.postDelayed(() -> {
+                        mCurrentDialog.onAuthenticationFailed(modality,
+                                mContext.getString(R.string.face_sensor_privacy_enabled));
+                    }, SENSOR_PRIVACY_DELAY);
+                } else {
+                    mCurrentDialog.onAuthenticationFailed(modality, errorMessage);
+                }
             } else {
                 final String errorMessage = getErrorString(modality, error, vendorCode);
                 if (DEBUG) Log.d(TAG, "onBiometricError, hard error: " + errorMessage);
                 mCurrentDialog.onError(modality, errorMessage);
             }
+
         } else {
             Log.w(TAG, "onBiometricError callback but dialog is gone");
         }
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 26c89ff..ba4257f 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
@@ -78,7 +78,7 @@
     private static final String TAG = "InternetDialog";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    static final long PROGRESS_DELAY_MS = 2000L;
+    static final long PROGRESS_DELAY_MS = 1500L;
 
     private final Handler mHandler;
     private final Executor mBackgroundExecutor;
@@ -137,6 +137,8 @@
     protected WifiEntry mConnectedWifiEntry;
     @VisibleForTesting
     protected int mWifiEntriesCount;
+    @VisibleForTesting
+    protected boolean mHasMoreEntry;
 
     // Wi-Fi scanning progress bar
     protected boolean mIsProgressBarVisible;
@@ -464,8 +466,7 @@
         }
         mWifiRecyclerView.setMinimumHeight(mWifiNetworkHeight * getWifiListMaxCount());
         mWifiRecyclerView.setVisibility(View.VISIBLE);
-        final boolean showSeeAll = mConnectedWifiEntry != null || mWifiEntriesCount > 0;
-        mSeeAllLayout.setVisibility(showSeeAll ? View.VISIBLE : View.INVISIBLE);
+        mSeeAllLayout.setVisibility(mHasMoreEntry ? View.VISIBLE : View.INVISIBLE);
     }
 
     @VisibleForTesting
@@ -549,9 +550,13 @@
     }
 
     private void setProgressBarVisible(boolean visible) {
+        if (mIsProgressBarVisible == visible) {
+            return;
+        }
         mIsProgressBarVisible = visible;
-        mProgressBar.setVisibility(mIsProgressBarVisible ? View.VISIBLE : View.GONE);
-        mDivider.setVisibility(mIsProgressBarVisible ? View.GONE : View.VISIBLE);
+        mProgressBar.setVisibility(visible ? View.VISIBLE : View.GONE);
+        mProgressBar.setIndeterminate(visible);
+        mDivider.setVisibility(visible ? View.GONE : View.VISIBLE);
         mInternetDialogSubTitle.setText(getSubtitleText());
     }
 
@@ -651,13 +656,14 @@
     @Override
     @WorkerThread
     public void onAccessPointsChanged(@Nullable List<WifiEntry> wifiEntries,
-            @Nullable WifiEntry connectedEntry) {
+            @Nullable WifiEntry connectedEntry, boolean hasMoreEntry) {
         // Should update the carrier network layout when it is connected under airplane mode ON.
         boolean shouldUpdateCarrierNetwork = mMobileNetworkLayout.getVisibility() == View.VISIBLE
                 && mInternetDialogController.isAirplaneModeEnabled();
         mHandler.post(() -> {
             mConnectedWifiEntry = connectedEntry;
             mWifiEntriesCount = wifiEntries == null ? 0 : wifiEntries.size();
+            mHasMoreEntry = hasMoreEntry;
             updateDialog(shouldUpdateCarrierNetwork /* shouldUpdateMobileNetwork */);
             mAdapter.setWifiEntries(wifiEntries, mWifiEntriesCount);
             mAdapter.notifyDataSetChanged();
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 6f63a08..1fee1b4 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
@@ -348,6 +348,10 @@
             return mContext.getText(SUBTITLE_TEXT_SEARCHING_FOR_NETWORKS);
         }
 
+        if (isCarrierNetworkActive()) {
+            return mContext.getText(SUBTITLE_TEXT_NON_CARRIER_NETWORK_UNAVAILABLE);
+        }
+
         // Sub-Title:
         // show non_carrier_network_unavailable
         //   - while Wi-Fi on + no Wi-Fi item
@@ -879,20 +883,25 @@
             mConnectedEntry = null;
             mWifiEntriesCount = 0;
             if (mCallback != null) {
-                mCallback.onAccessPointsChanged(null /* wifiEntries */, null /* connectedEntry */);
+                mCallback.onAccessPointsChanged(null /* wifiEntries */, null /* connectedEntry */,
+                        false /* hasMoreEntry */);
             }
             return;
         }
 
+        boolean hasMoreEntry = false;
         int count = MAX_WIFI_ENTRY_COUNT;
         if (mHasEthernet) {
             count -= 1;
         }
-        if (hasActiveSubId()) {
+        if (hasActiveSubId() || isCarrierNetworkActive()) {
             count -= 1;
         }
-        if (count > accessPoints.size()) {
-            count = accessPoints.size();
+        final int wifiTotalCount = accessPoints.size();
+        if (count > wifiTotalCount) {
+            count = wifiTotalCount;
+        } else if (count < wifiTotalCount) {
+            hasMoreEntry = true;
         }
 
         WifiEntry connectedEntry = null;
@@ -909,7 +918,7 @@
         mWifiEntriesCount = wifiEntries.size();
 
         if (mCallback != null) {
-            mCallback.onAccessPointsChanged(wifiEntries, mConnectedEntry);
+            mCallback.onAccessPointsChanged(wifiEntries, mConnectedEntry, hasMoreEntry);
         }
     }
 
@@ -1060,7 +1069,7 @@
         void dismissDialog();
 
         void onAccessPointsChanged(@Nullable List<WifiEntry> wifiEntries,
-                @Nullable WifiEntry connectedEntry);
+                @Nullable WifiEntry connectedEntry, boolean hasMoreEntry);
     }
 
     void makeOverlayToast(int stringId) {
diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseDialog.kt b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseDialog.kt
index c50365f..71c5fad 100644
--- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseDialog.kt
@@ -15,7 +15,8 @@
 class SensorUseDialog(
     context: Context,
     val sensor: Int,
-    val clickListener: DialogInterface.OnClickListener
+    val clickListener: DialogInterface.OnClickListener,
+    val dismissListener: DialogInterface.OnDismissListener
 ) : SystemUIDialog(context) {
 
     // TODO move to onCreate (b/200815309)
@@ -69,6 +70,8 @@
                 context.getString(com.android.internal.R.string
                         .cancel), clickListener)
 
+        setOnDismissListener(dismissListener)
+
         setCancelable(false)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
index b0071d9..dae375a 100644
--- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
@@ -50,7 +50,7 @@
     private val keyguardStateController: KeyguardStateController,
     private val keyguardDismissUtil: KeyguardDismissUtil,
     @Background private val bgHandler: Handler
-) : Activity(), DialogInterface.OnClickListener {
+) : Activity(), DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
 
     companion object {
         private val LOG_TAG = SensorUseStartedActivity::class.java.simpleName
@@ -120,7 +120,7 @@
             }
         }
 
-        mDialog = SensorUseDialog(this, sensor, this)
+        mDialog = SensorUseDialog(this, sensor, this, this)
         mDialog!!.show()
     }
 
@@ -212,4 +212,8 @@
                     .suppressSensorPrivacyReminders(sensor, suppressed)
         }
     }
+
+    override fun onDismiss(dialog: DialogInterface?) {
+        finish()
+    }
 }
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 95e7a98c..6dca2a7 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
@@ -159,7 +159,6 @@
         mAccessPoints.add(mWifiEntry1);
         when(mAccessPointController.getMergedCarrierEntry()).thenReturn(mMergedCarrierEntry);
         when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{SUB_ID});
-        when(mAccessPointController.getMergedCarrierEntry()).thenReturn(mMergedCarrierEntry);
         when(mToastFactory.createToast(any(), anyString(), anyString(), anyInt(), anyInt()))
             .thenReturn(mSystemUIToast);
         when(mSystemUIToast.getView()).thenReturn(mToastView);
@@ -335,6 +334,17 @@
     }
 
     @Test
+    public void getSubtitleText_withCarrierNetworkActiveOnly_returnNoOtherAvailable() {
+        fakeAirplaneModeEnabled(false);
+        when(mWifiManager.isWifiEnabled()).thenReturn(true);
+        mInternetDialogController.onAccessPointsChanged(null /* accessPoints */);
+        when(mMergedCarrierEntry.isDefaultNetwork()).thenReturn(true);
+
+        assertThat(mInternetDialogController.getSubtitleText(false))
+                .isEqualTo(getResourcesString("non_carrier_network_unavailable"));
+    }
+
+    @Test
     public void getWifiDetailsSettingsIntent_withNoKey_returnNull() {
         assertThat(mInternetDialogController.getWifiDetailsSettingsIntent(null)).isNull();
     }
@@ -400,7 +410,7 @@
 
         mInternetDialogController.onAccessPointsChanged(null /* accessPoints */);
 
-        verify(mInternetDialogCallback, never()).onAccessPointsChanged(any(), any());
+        verify(mInternetDialogCallback, never()).onAccessPointsChanged(any(), any(), anyBoolean());
     }
 
     @Test
@@ -409,8 +419,8 @@
 
         mInternetDialogController.onAccessPointsChanged(null /* accessPoints */);
 
-        verify(mInternetDialogCallback)
-                .onAccessPointsChanged(null /* wifiEntries */, null /* connectedEntry */);
+        verify(mInternetDialogCallback).onAccessPointsChanged(null /* wifiEntries */,
+                null /* connectedEntry */, false /* hasMoreEntry */);
     }
 
     @Test
@@ -423,7 +433,8 @@
         mInternetDialogController.onAccessPointsChanged(mAccessPoints);
 
         mWifiEntries.clear();
-        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry,
+                false /* hasMoreEntry */);
     }
 
     @Test
@@ -437,8 +448,8 @@
 
         mWifiEntries.clear();
         mWifiEntries.add(mWifiEntry1);
-        verify(mInternetDialogCallback)
-                .onAccessPointsChanged(mWifiEntries, null /* connectedEntry */);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries,
+                null /* connectedEntry */, false /* hasMoreEntry */);
     }
 
     @Test
@@ -453,7 +464,8 @@
 
         mWifiEntries.clear();
         mWifiEntries.add(mWifiEntry1);
-        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry,
+                false /* hasMoreEntry */);
     }
 
     @Test
@@ -470,7 +482,8 @@
         mWifiEntries.clear();
         mWifiEntries.add(mWifiEntry1);
         mWifiEntries.add(mWifiEntry2);
-        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry,
+                false /* hasMoreEntry */);
     }
 
     @Test
@@ -489,7 +502,8 @@
         mWifiEntries.add(mWifiEntry1);
         mWifiEntries.add(mWifiEntry2);
         mWifiEntries.add(mWifiEntry3);
-        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry,
+                false /* hasMoreEntry */);
 
         // Turn off airplane mode to has carrier network, then Wi-Fi entries will cut last one.
         reset(mInternetDialogCallback);
@@ -498,7 +512,8 @@
         mInternetDialogController.onAccessPointsChanged(mAccessPoints);
 
         mWifiEntries.remove(mWifiEntry3);
-        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry,
+                true /* hasMoreEntry */);
     }
 
     @Test
@@ -518,7 +533,8 @@
         mWifiEntries.add(mWifiEntry1);
         mWifiEntries.add(mWifiEntry2);
         mWifiEntries.add(mWifiEntry3);
-        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry,
+                true /* hasMoreEntry */);
 
         // Turn off airplane mode to has carrier network, then Wi-Fi entries will cut last one.
         reset(mInternetDialogCallback);
@@ -527,7 +543,38 @@
         mInternetDialogController.onAccessPointsChanged(mAccessPoints);
 
         mWifiEntries.remove(mWifiEntry3);
-        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries, mConnectedEntry,
+                true /* hasMoreEntry */);
+    }
+
+    @Test
+    public void onAccessPointsChanged_oneCarrierWifiAndFourOthers_callbackCutMore() {
+        reset(mInternetDialogCallback);
+        fakeAirplaneModeEnabled(true);
+        when(mMergedCarrierEntry.isDefaultNetwork()).thenReturn(true);
+        mAccessPoints.clear();
+        mAccessPoints.add(mWifiEntry1);
+        mAccessPoints.add(mWifiEntry2);
+        mAccessPoints.add(mWifiEntry3);
+        mAccessPoints.add(mWifiEntry4);
+
+        mInternetDialogController.onAccessPointsChanged(mAccessPoints);
+
+        mWifiEntries.clear();
+        mWifiEntries.add(mWifiEntry1);
+        mWifiEntries.add(mWifiEntry2);
+        mWifiEntries.add(mWifiEntry3);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries,
+                null /* connectedEntry */, true /* hasMoreEntry */);
+
+        // Turn off airplane mode to has carrier WiFi, then Wi-Fi entries will keep the same.
+        reset(mInternetDialogCallback);
+        fakeAirplaneModeEnabled(false);
+
+        mInternetDialogController.onAccessPointsChanged(mAccessPoints);
+
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries,
+                null /* connectedEntry */, true /* hasMoreEntry */);
     }
 
     @Test
@@ -547,8 +594,8 @@
         mWifiEntries.add(mWifiEntry2);
         mWifiEntries.add(mWifiEntry3);
         mWifiEntries.add(mWifiEntry4);
-        verify(mInternetDialogCallback)
-                .onAccessPointsChanged(mWifiEntries, null /* connectedEntry */);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries,
+                null /* connectedEntry */, false /* hasMoreEntry */);
 
         // If the Ethernet exists, then Wi-Fi entries will cut last one.
         reset(mInternetDialogCallback);
@@ -557,8 +604,8 @@
         mInternetDialogController.onAccessPointsChanged(mAccessPoints);
 
         mWifiEntries.remove(mWifiEntry4);
-        verify(mInternetDialogCallback)
-                .onAccessPointsChanged(mWifiEntries, null /* connectedEntry */);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries,
+                null /* connectedEntry */, true /* hasMoreEntry */);
 
         // Turn off airplane mode to has carrier network, then Wi-Fi entries will cut last one.
         reset(mInternetDialogCallback);
@@ -567,8 +614,8 @@
         mInternetDialogController.onAccessPointsChanged(mAccessPoints);
 
         mWifiEntries.remove(mWifiEntry3);
-        verify(mInternetDialogCallback)
-                .onAccessPointsChanged(mWifiEntries, null /* connectedEntry */);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries,
+                null /* connectedEntry */, true /* hasMoreEntry */);
     }
 
     @Test
@@ -584,8 +631,8 @@
 
         mWifiEntries.clear();
         mWifiEntries.add(mWifiEntry1);
-        verify(mInternetDialogCallback)
-                .onAccessPointsChanged(mWifiEntries, null /* connectedEntry */);
+        verify(mInternetDialogCallback).onAccessPointsChanged(mWifiEntries,
+                null /* connectedEntry */, false /* hasMoreEntry */);
     }
 
     @Test
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 0cf063f..651bcde 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
@@ -316,6 +316,20 @@
     }
 
     @Test
+    public void updateDialog_wifiOnAndOneWifiEntry_showWifiListAndSeeAllArea() {
+        // The precondition WiFi ON is already in setUp()
+        mInternetDialog.mConnectedWifiEntry = null;
+        mInternetDialog.mWifiEntriesCount = 1;
+
+        mInternetDialog.updateDialog(false);
+
+        assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.GONE);
+        // Show a blank block to fix the dialog height even if there is no WiFi list
+        assertThat(mWifiList.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mSeeAll.getVisibility()).isEqualTo(View.INVISIBLE);
+    }
+
+    @Test
     public void updateDialog_wifiOnAndHasConnectedWifi_showAllWifiAndSeeAllArea() {
         // The preconditions WiFi ON and WiFi entries are already in setUp()
         mInternetDialog.mWifiEntriesCount = 0;
@@ -325,13 +339,15 @@
         assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.VISIBLE);
         // Show a blank block to fix the dialog height even if there is no WiFi list
         assertThat(mWifiList.getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(mSeeAll.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mSeeAll.getVisibility()).isEqualTo(View.INVISIBLE);
     }
 
     @Test
-    public void updateDialog_wifiOnAndHasWifiList_showWifiListAndSeeAll() {
+    public void updateDialog_wifiOnAndHasMaxWifiList_showWifiListAndSeeAll() {
         // The preconditions WiFi ON and WiFi entries are already in setUp()
         mInternetDialog.mConnectedWifiEntry = null;
+        mInternetDialog.mWifiEntriesCount = MAX_WIFI_ENTRY_COUNT;
+        mInternetDialog.mHasMoreEntry = true;
 
         mInternetDialog.updateDialog(false);
 
@@ -343,6 +359,8 @@
     @Test
     public void updateDialog_wifiOnAndHasBothWifiEntry_showBothWifiEntryAndSeeAll() {
         // The preconditions WiFi ON and WiFi entries are already in setUp()
+        mInternetDialog.mWifiEntriesCount = MAX_WIFI_ENTRY_COUNT - 1;
+        mInternetDialog.mHasMoreEntry = true;
 
         mInternetDialog.updateDialog(false);
 
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 7938511..89435ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -6,6 +6,7 @@
 import android.testing.TestableLooper.RunWithLooper
 import android.util.DisplayMetrics
 import com.android.systemui.ExpandHelper
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.media.MediaHierarchyManager
@@ -81,6 +82,8 @@
                 mDependency,
                 TestableLooper.get(this))
         row = helper.createRow()
+        context.getOrCreateTestableResources()
+                .addOverride(R.bool.config_use_split_notification_shade, false)
         transitionController = LockscreenShadeTransitionController(
             statusBarStateController = statusbarStateController,
             lockscreenGestureLogger = lockscreenGestureLogger,
diff --git a/services/backup/backuplib/java/com/android/server/backup/TransportManager.java b/services/backup/backuplib/java/com/android/server/backup/TransportManager.java
index 594140e..21a22f4 100644
--- a/services/backup/backuplib/java/com/android/server/backup/TransportManager.java
+++ b/services/backup/backuplib/java/com/android/server/backup/TransportManager.java
@@ -39,6 +39,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.backup.IBackupTransport;
 import com.android.internal.util.Preconditions;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.OnTransportRegisteredListener;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.transport.TransportConnectionManager;
@@ -641,7 +642,7 @@
         TransportConnection transportConnection =
                 mTransportConnectionManager.getTransportClient(
                         transportComponent, extras, callerLogString);
-        final IBackupTransport transport;
+        final BackupTransportClient transport;
         try {
             transport = transportConnection.connectOrThrow(callerLogString);
         } catch (TransportNotAvailableException e) {
@@ -653,10 +654,6 @@
 
         int result;
         try {
-            // This is a temporary fix to allow blocking calls.
-            // TODO: b/147702043. Redesign IBackupTransport so as to make the calls non-blocking.
-            Binder.allowBlocking(transport.asBinder());
-
             String transportName = transport.name();
             String transportDirName = transport.transportDirName();
             registerTransport(transportComponent, transport);
@@ -674,8 +671,8 @@
     }
 
     /** If {@link RemoteException} is thrown the transport is guaranteed to not be registered. */
-    private void registerTransport(ComponentName transportComponent, IBackupTransport transport)
-            throws RemoteException {
+    private void registerTransport(ComponentName transportComponent,
+            BackupTransportClient transport) throws RemoteException {
         checkCanUseTransport();
 
         TransportDescription description =
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
index a3f6eb6..85ab48c 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
+++ b/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
@@ -21,6 +21,7 @@
 import android.app.backup.RestoreSet;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
+import android.os.Binder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 
@@ -35,6 +36,10 @@
 
     BackupTransportClient(IBackupTransport transportBinder) {
         mTransportBinder = transportBinder;
+
+        // This is a temporary fix to allow blocking calls.
+        // TODO: b/147702043. Redesign IBackupTransport so as to make the calls non-blocking.
+        Binder.allowBlocking(mTransportBinder.asBinder());
     }
 
     /**
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java b/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java
index da77eba..f9a3c36 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java
+++ b/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java
@@ -59,7 +59,7 @@
 import java.util.concurrent.ExecutionException;
 
 /**
- * A {@link TransportConnection} manages the connection to an {@link IBackupTransport} service,
+ * A {@link TransportConnection} manages the connection to a {@link BackupTransportClient},
  * obtained via the {@param bindIntent} parameter provided in the constructor. A
  * {@link TransportConnection} is responsible for only one connection to the transport service,
  * not more.
@@ -67,9 +67,9 @@
  * <p>After retrieved using {@link TransportManager#getTransportClient(String, String)}, you can
  * call either {@link #connect(String)}, if you can block your thread, or {@link
  * #connectAsync(TransportConnectionListener, String)}, otherwise, to obtain a {@link
- * IBackupTransport} instance. It's meant to be passed around as a token to a connected transport.
- * When the connection is not needed anymore you should call {@link #unbind(String)} or indirectly
- * via {@link TransportManager#disposeOfTransportClient(TransportConnection, String)}.
+ * BackupTransportClient} instance. It's meant to be passed around as a token to a connected
+ * transport. When the connection is not needed anymore you should call {@link #unbind(String)} or
+ * indirectly via {@link TransportManager#disposeOfTransportClient(TransportConnection, String)}.
  *
  * <p>DO NOT forget to unbind otherwise there will be dangling connections floating around.
  *
@@ -106,7 +106,7 @@
     private int mState = State.IDLE;
 
     @GuardedBy("mStateLock")
-    private volatile IBackupTransport mTransport;
+    private volatile BackupTransportClient mTransport;
 
     TransportConnection(
             @UserIdInt int userId,
@@ -174,10 +174,12 @@
      * trigger another one, just piggyback on the original request.
      *
      * <p>It's guaranteed that you are going to get a call back to {@param listener} after this
-     * call. However, the {@param IBackupTransport} parameter, the transport binder, is not
-     * guaranteed to be non-null, or if it's non-null it's not guaranteed to be usable - i.e. it can
-     * throw {@link DeadObjectException}s on method calls. You should check for both in your code.
-     * The reasons for a null transport binder are:
+     * call. However, the {@link BackupTransportClient} parameter in
+     * {@link TransportConnectionListener#onTransportConnectionResult(BackupTransportClient,
+     * TransportConnection)}, the transport client, is not guaranteed to be non-null, or if it's
+     * non-null it's not guaranteed to be usable - i.e. it can throw {@link DeadObjectException}s
+     * on method calls. You should check for both in your code. The reasons for a null transport
+     * client are:
      *
      * <ul>
      *   <li>Some code called {@link #unbind(String)} before you got a callback.
@@ -193,7 +195,7 @@
      * For unusable transport binders check {@link DeadObjectException}.
      *
      * @param listener The listener that will be called with the (possibly null or unusable) {@link
-     *     IBackupTransport} instance and this {@link TransportConnection} object.
+     *     BackupTransportClient} instance and this {@link TransportConnection} object.
      * @param caller A {@link String} identifying the caller for logging/debugging purposes. This
      *     should be a human-readable short string that is easily identifiable in the logs. Ideally
      *     TAG.methodName(), where TAG is the one used in logcat. In cases where this is is not very
@@ -293,8 +295,8 @@
      *
      * <p>Synchronous version of {@link #connectAsync(TransportConnectionListener, String)}. The
      * same observations about state are valid here. Also, what was said about the {@link
-     * IBackupTransport} parameter of {@link TransportConnectionListener} now apply to the return
-     * value of this method.
+     * BackupTransportClient} parameter of {@link TransportConnectionListener} now apply to the
+     * return value of this method.
      *
      * <p>This is a potentially blocking operation, so be sure to call this carefully on the correct
      * threads. You can't call this from the process main-thread (it throws an exception if you do
@@ -305,18 +307,18 @@
      *
      * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
      *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
-     * @return A {@link IBackupTransport} transport binder instance or null. If it's non-null it can
-     *     still be unusable - throws {@link DeadObjectException} on method calls
+     * @return A {@link BackupTransportClient} transport client instance or null. If it's non-null
+     *     it can still be unusable - throws {@link DeadObjectException} on method calls
      */
     @WorkerThread
     @Nullable
-    public IBackupTransport connect(String caller) {
+    public BackupTransportClient connect(String caller) {
         // If called on the main-thread this could deadlock waiting because calls to
         // ServiceConnection are on the main-thread as well
         Preconditions.checkState(
                 !Looper.getMainLooper().isCurrentThread(), "Can't call connect() on main thread");
 
-        IBackupTransport transport = mTransport;
+        BackupTransportClient transport = mTransport;
         if (transport != null) {
             log(Priority.DEBUG, caller, "Sync connect: reusing transport");
             return transport;
@@ -330,7 +332,7 @@
             }
         }
 
-        CompletableFuture<IBackupTransport> transportFuture = new CompletableFuture<>();
+        CompletableFuture<BackupTransportClient> transportFuture = new CompletableFuture<>();
         TransportConnectionListener requestListener =
                 (requestedTransport, transportClient) ->
                         transportFuture.complete(requestedTransport);
@@ -359,13 +361,14 @@
      *
      * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
      *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
-     * @return A {@link IBackupTransport} transport binder instance.
+     * @return A {@link BackupTransportClient} transport binder instance.
      * @see #connect(String)
      * @throws TransportNotAvailableException if connection attempt fails.
      */
     @WorkerThread
-    public IBackupTransport connectOrThrow(String caller) throws TransportNotAvailableException {
-        IBackupTransport transport = connect(caller);
+    public BackupTransportClient connectOrThrow(String caller)
+            throws TransportNotAvailableException {
+        BackupTransportClient transport = connect(caller);
         if (transport == null) {
             log(Priority.ERROR, caller, "Transport connection failed");
             throw new TransportNotAvailableException();
@@ -379,12 +382,12 @@
      *
      * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
      *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
-     * @return A {@link IBackupTransport} transport binder instance.
+     * @return A {@link BackupTransportClient} transport client instance.
      * @throws TransportNotAvailableException if not connected.
      */
-    public IBackupTransport getConnectedTransport(String caller)
+    public BackupTransportClient getConnectedTransport(String caller)
             throws TransportNotAvailableException {
-        IBackupTransport transport = mTransport;
+        BackupTransportClient transport = mTransport;
         if (transport == null) {
             log(Priority.ERROR, caller, "Transport not connected");
             throw new TransportNotAvailableException();
@@ -425,7 +428,8 @@
     }
 
     private void onServiceConnected(IBinder binder) {
-        IBackupTransport transport = IBackupTransport.Stub.asInterface(binder);
+        IBackupTransport transportBinder = IBackupTransport.Stub.asInterface(binder);
+        BackupTransportClient transport = new BackupTransportClient(transportBinder);
         synchronized (mStateLock) {
             checkStateIntegrityLocked();
 
@@ -492,15 +496,15 @@
 
     private void notifyListener(
             TransportConnectionListener listener,
-            @Nullable IBackupTransport transport,
+            @Nullable BackupTransportClient transport,
             String caller) {
-        String transportString = (transport != null) ? "IBackupTransport" : "null";
+        String transportString = (transport != null) ? "BackupTransportClient" : "null";
         log(Priority.INFO, "Notifying [" + caller + "] transport = " + transportString);
         mListenerHandler.post(() -> listener.onTransportConnectionResult(transport, this));
     }
 
     @GuardedBy("mStateLock")
-    private void notifyListenersAndClearLocked(@Nullable IBackupTransport transport) {
+    private void notifyListenersAndClearLocked(@Nullable BackupTransportClient transport) {
         for (Map.Entry<TransportConnectionListener, String> entry : mListeners.entrySet()) {
             TransportConnectionListener listener = entry.getKey();
             String caller = entry.getValue();
@@ -510,7 +514,7 @@
     }
 
     @GuardedBy("mStateLock")
-    private void setStateLocked(@State int state, @Nullable IBackupTransport transport) {
+    private void setStateLocked(@State int state, @Nullable BackupTransportClient transport) {
         log(Priority.VERBOSE, "State: " + stateToString(mState) + " => " + stateToString(state));
         onStateTransition(mState, state);
         mState = state;
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionListener.java b/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionListener.java
index 03d35e4..1776c41 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionListener.java
+++ b/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionListener.java
@@ -18,7 +18,7 @@
 
 import android.annotation.Nullable;
 
-import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.transport.BackupTransportClient;
 
 /**
  * Listener to be called by {@link TransportConnection#connectAsync(TransportConnectionListener,
@@ -26,13 +26,14 @@
  */
 public interface TransportConnectionListener {
     /**
-     * Called when {@link TransportConnection} has a transport binder available or that it decided
+     * Called when {@link TransportConnection} has a transport client available or that it decided
      * it couldn't obtain one, in which case {@param transport} is null.
      *
-     * @param transport A {@link IBackupTransport} transport binder or null.
+     * @param transportClient A {@link BackupTransportClient} transport or null.
      * @param transportConnection The {@link TransportConnection} used to retrieve this transport
-     *                            binder.
+     *                            client.
      */
     void onTransportConnectionResult(
-            @Nullable IBackupTransport transport, TransportConnection transportConnection);
+            @Nullable BackupTransportClient transportClient,
+            TransportConnection transportConnection);
 }
diff --git a/services/backup/java/com/android/server/backup/OperationStorage.java b/services/backup/java/com/android/server/backup/OperationStorage.java
new file mode 100644
index 0000000..466f647
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/OperationStorage.java
@@ -0,0 +1,156 @@
+/*
+ * 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.backup;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+
+/**
+ * OperationStorage is an abstraction around a set of active operations.
+ *
+ * Operations are registered with a token that must first be obtained from
+ * {@link UserBackupManagerService#generateRandomIntegerToken()}.  When
+ * registering, the caller may also associate a set of package names with
+ * the operation.
+ *
+ * TODO(b/208442527): have the token be generated within and returned by
+ *                    registerOperation, as it should be an internal detail.
+ *
+ * Operations have a type and a state.  Although ints, the values that can
+ * be used are defined in {@link UserBackupManagerService}.  If the type of
+ * an operation is OP_BACKUP, then it represents a task running backups. The
+ * task is provided when registering the operation because it provides a
+ * handle to cancel the backup.
+ */
+public interface OperationStorage {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+        OpState.PENDING,
+        OpState.ACKNOWLEDGED,
+        OpState.TIMEOUT
+    })
+    public @interface OpState {
+        // The operation is in progress.
+        int PENDING = 0;
+        // The operation has been acknowledged.
+        int ACKNOWLEDGED = 1;
+        // The operation has timed out.
+        int TIMEOUT = -1;
+    }
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+        OpType.BACKUP_WAIT,
+        OpType.RESTORE_WAIT,
+        OpType.BACKUP,
+    })
+    public @interface OpType {
+        // Waiting for backup agent to respond during backup operation.
+        int BACKUP_WAIT = 0;
+        // Waiting for backup agent to respond during restore operation.
+        int RESTORE_WAIT = 1;
+        // An entire backup operation spanning multiple packages.
+        int BACKUP = 2;
+    }
+
+    /**
+     * Record an ongoing operation of given type and in the given initial
+     * state. The associated task is used as a callback.
+     *
+     * @param token        an operation token issued by
+     *                     {@link UserBackupManagerService#generateRandomIntegerToken()}
+     * @param initialState the state that the operation starts in
+     * @param task         the {@link BackupRestoreTask} that is expected to
+     *                     remove the operation on completion, and which may
+     *                     be notified if the operation requires cancelling.
+     * @param type         the type of the operation.
+     */
+    void registerOperation(int token, @OpState int initialState,
+            BackupRestoreTask task, @OpType int type);
+
+    /**
+     * See {@link #registerOperation()}.  In addition this method accepts a set
+     * of package names which are associated with the operation.
+     *
+     * @param token        See {@link #registerOperation()}
+     * @param initialState See {@link #registerOperation()}
+     * @param packageNames the package names to associate with the operation.
+     * @param task         See {@link #registerOperation()}
+     * @param type         See {@link #registerOperation()}
+     */
+    void registerOperationForPackages(int token, @OpState int initialState,
+            Set<String> packageNames, BackupRestoreTask task, @OpType int type);
+
+    /**
+     * Remove the operation identified by token.  This is called when the
+     * operation is no longer in progress and should be dropped. Any association
+     * with package names provided in {@link #registerOperation()} is dropped as
+     * well.
+     *
+     * @param token the operation token specified when registering the operation.
+     */
+    void removeOperation(int token);
+
+    /**
+     * Obtain the number of currently registered operations.
+     *
+     * @return the number of currently registered operations.
+     */
+    int numOperations();
+
+    /**
+     * Determine if a backup operation is in progress or not.
+     *
+     * @return true if any operation is registered of type BACKUP and in
+     *         state PENDING.
+     */
+    boolean isBackupOperationInProgress();
+
+    /**
+     * Obtain a set of operation tokens for all pending operations that were
+     * registered with an association to the specified package name.
+     *
+     * @param packageName the name of the package used at registration time
+     *
+     * @return a set of operation tokens associated to package name.
+     */
+    Set<Integer> operationTokensForPackage(String packageName);
+
+    /**
+     * Obtain a set of operation tokens for all pending operations that are
+     * of the specified operation type.
+     *
+     * @param type the type of the operation provided at registration time.
+     *
+     * @return a set of operation tokens for operations of that type.
+     */
+    Set<Integer> operationTokensForOpType(@OpType int type);
+
+    /**
+     * Obtain a set of operation tokens for all pending operations that are
+     * currently in the specified operation state.
+     *
+     * @param state the state of the operation.
+     *
+     * @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/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 452adb2..98ea03e 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -103,7 +103,6 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.backup.IBackupTransport;
 import com.android.internal.util.Preconditions;
 import com.android.server.AppWidgetBackupBridge;
 import com.android.server.EventLogTags;
@@ -127,6 +126,7 @@
 import com.android.server.backup.params.RestoreParams;
 import com.android.server.backup.restore.ActiveRestoreSession;
 import com.android.server.backup.restore.PerformUnifiedRestoreTask;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.transport.TransportNotAvailableException;
 import com.android.server.backup.transport.TransportNotRegisteredException;
@@ -3719,7 +3719,8 @@
                 mTransportManager.getTransportClient(newTransportName, callerLogString);
         if (transportConnection != null) {
             try {
-                IBackupTransport transport = transportConnection.connectOrThrow(callerLogString);
+                BackupTransportClient transport = transportConnection.connectOrThrow(
+                        callerLogString);
                 mCurrentToken = transport.getCurrentRestoreSet();
             } catch (Exception e) {
                 // Oops.  We can't know the current dataset token, so reset and figure it out
@@ -4371,7 +4372,7 @@
 
         final long oldCallingId = Binder.clearCallingIdentity();
         try {
-            IBackupTransport transport = transportConnection.connectOrThrow(
+            BackupTransportClient transport = transportConnection.connectOrThrow(
                     /* caller */ "BMS.getOperationTypeFromTransport");
             if ((transport.getTransportFlags() & BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER) != 0) {
                 return OperationType.MIGRATION;
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 1c86091..9ce4eab 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -41,7 +41,6 @@
 import android.util.Log;
 import android.util.Slog;
 
-import com.android.internal.backup.IBackupTransport;
 import com.android.server.EventLogTags;
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
@@ -51,6 +50,7 @@
 import com.android.server.backup.internal.OnTaskFinishedListener;
 import com.android.server.backup.internal.Operation;
 import com.android.server.backup.remote.RemoteCall;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.transport.TransportNotAvailableException;
 import com.android.server.backup.utils.BackupEligibilityRules;
@@ -300,7 +300,7 @@
                 mUserBackupManagerService.handleCancel(mBackupRunnerOpToken, cancelAll);
                 try {
                     // If we're running a backup we should be connected to a transport
-                    IBackupTransport transport =
+                    BackupTransportClient transport =
                             mTransportConnection.getConnectedTransport("PFTBT.handleCancel()");
                     transport.cancelFullBackup();
                 } catch (RemoteException | TransportNotAvailableException e) {
@@ -353,7 +353,7 @@
                 return;
             }
 
-            IBackupTransport transport = mTransportConnection.connect("PFTBT.run()");
+            BackupTransportClient transport = mTransportConnection.connect("PFTBT.run()");
             if (transport == null) {
                 Slog.w(TAG, "Transport not present; full data backup not performed");
                 backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
@@ -745,7 +745,7 @@
                     Slog.v(TAG, "Got preflight response; size=" + totalSize);
                 }
 
-                IBackupTransport transport =
+                BackupTransportClient transport =
                         mTransportConnection.connectOrThrow("PFTBT$SPBP.preflightFullBackup()");
                 result = transport.checkFullBackupSize(totalSize);
                 if (result == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
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 3b3bf8c6..5c24859 100644
--- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java
+++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
@@ -31,7 +31,6 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.backup.IBackupTransport;
 import com.android.server.EventLogTags;
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
@@ -51,6 +50,7 @@
 import com.android.server.backup.params.RestoreParams;
 import com.android.server.backup.restore.PerformAdbRestoreTask;
 import com.android.server.backup.restore.PerformUnifiedRestoreTask;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 
 import java.util.ArrayList;
@@ -149,7 +149,7 @@
                 String callerLogString = "BH/MSG_RUN_BACKUP";
                 TransportConnection transportConnection =
                         transportManager.getCurrentTransportClient(callerLogString);
-                IBackupTransport transport =
+                BackupTransportClient transport =
                         transportConnection != null
                                 ? transportConnection.connect(callerLogString)
                                 : null;
@@ -364,7 +364,7 @@
                 RestoreGetSetsParams params = (RestoreGetSetsParams) msg.obj;
                 String callerLogString = "BH/MSG_RUN_GET_RESTORE_SETS";
                 try {
-                    IBackupTransport transport =
+                    BackupTransportClient transport =
                             params.mTransportConnection.connectOrThrow(callerLogString);
                     sets = transport.getAvailableRestoreSets();
                     // cache the result in the active session
diff --git a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
new file mode 100644
index 0000000..6908c60
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
@@ -0,0 +1,356 @@
+/*
+ * 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.backup.internal;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+
+import android.annotation.UserIdInt;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.OperationStorage;
+
+import com.google.android.collect.Sets;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+/**
+ * LifecycleOperationStorage is responsible for maintaining a set of currently
+ * active operations.  Each operation has a type and state, and a callback that
+ * can receive events upon operation completion or cancellation.  It may also
+ * be associated with one or more package names.
+ *
+ * An operation wraps a {@link BackupRestoreTask} within it.
+ * It's the responsibility of this task to remove the operation from this array.
+ *
+ * If type of operation is {@code OP_TYPE_WAIT}, it is waiting for an ACK or
+ * timeout.
+ *
+ * A BackupRestore task gets notified of AVK/timeout for the operation via
+ * {@link BackupRestoreTask#handleCancel()},
+ * {@link BackupRestoreTask#operationComplete()} and {@code notifyAll} called
+ * on the {@code mCurrentOpLock}.
+ *
+ * {@link LifecycleOperationStorage#waitUntilOperationComplete(int)} is used in
+ * various places to 'wait' for notifyAll and detect change of pending state of
+ * an operation. So typically, an operation will be removed from this array by:
+ * - {@link BackupRestoreTask#handleCancel()} and
+ * - {@link BackupRestoreTask#operationComplete()} OR
+ *   {@link BackupRestoreTask#waitUntilOperationComplete()}.
+ * Do not remove at both these places because {@code waitUntilOperationComplete}
+ * relies on the operation being present to determine its completion status.
+ *
+ * If type of operation is {@code OP_BACKUP}, it is a task running backups. It
+ * provides a handle to cancel backup tasks.
+ */
+public class LifecycleOperationStorage implements OperationStorage {
+    private static final String TAG = "LifecycleOperationStorage";
+
+    private final int mUserId;
+
+    private final Object mOperationsLock = new Object();
+
+    // Bookkeeping of in-flight operations. The operation token is the index of
+    // the entry in the pending operations list.
+    @GuardedBy("mOperationsLock")
+    private final SparseArray<Operation> mOperations = new SparseArray<>();
+
+    // Association from package name to one or more operations relating to that
+    // package.
+    @GuardedBy("mOperationsLock")
+    private final Map<String, Set<Integer>> mOpTokensByPackage = new HashMap<>();
+
+    public LifecycleOperationStorage(@UserIdInt int userId) {
+        this.mUserId = userId;
+    }
+
+    /** See {@link OperationStorage#registerOperation()} */
+    @Override
+    public void registerOperation(int token, @OpState int initialState,
+            BackupRestoreTask task, @OpType int type) {
+        registerOperationForPackages(token, initialState, Sets.newHashSet(), task, type);
+    }
+
+    /** See {@link OperationStorage#registerOperationForPackages()} */
+    @Override
+    public void registerOperationForPackages(int token, @OpState int initialState,
+            Set<String> packageNames, BackupRestoreTask task, @OpType int type) {
+        synchronized (mOperationsLock) {
+            mOperations.put(token, new Operation(initialState, task, type));
+            for (String packageName : packageNames) {
+                Set<Integer> tokens = mOpTokensByPackage.get(packageName);
+                if (tokens == null) {
+                    tokens = new HashSet<Integer>();
+                }
+                tokens.add(token);
+                mOpTokensByPackage.put(packageName, tokens);
+            }
+        }
+    }
+
+    /** See {@link OperationStorage#removeOperation()} */
+    @Override
+    public void removeOperation(int token) {
+        synchronized (mOperationsLock) {
+            mOperations.remove(token);
+            Set<String> packagesWithTokens = mOpTokensByPackage.keySet();
+            for (String packageName : packagesWithTokens) {
+                Set<Integer> tokens = mOpTokensByPackage.get(packageName);
+                if (tokens == null) {
+                    continue;
+                }
+                tokens.remove(token);
+                mOpTokensByPackage.put(packageName, tokens);
+            }
+        }
+    }
+
+    /** See {@link OperationStorage#numOperations()}. */
+    @Override
+    public int numOperations() {
+        synchronized (mOperationsLock) {
+            return mOperations.size();
+        }
+    }
+
+    /** See {@link OperationStorage#isBackupOperationInProgress()}. */
+    @Override
+    public boolean isBackupOperationInProgress() {
+        synchronized (mOperationsLock) {
+            for (int i = 0; i < mOperations.size(); i++) {
+                Operation op = mOperations.valueAt(i);
+                if (op.type == OpType.BACKUP && op.state == OpState.PENDING) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /** See {@link OperationStorage#operationTokensForPackage()} */
+    @Override
+    public Set<Integer> operationTokensForPackage(String packageName) {
+        synchronized (mOperationsLock) {
+            final Set<Integer> tokens = mOpTokensByPackage.get(packageName);
+            Set<Integer> result = Sets.newHashSet();
+            if (tokens != null) {
+                result.addAll(tokens);
+            }
+            return result;
+        }
+    }
+
+    /** See {@link OperationStorage#operationTokensForOpType()} */
+    @Override
+    public Set<Integer> operationTokensForOpType(@OpType int type) {
+        Set<Integer> tokens = Sets.newHashSet();
+        synchronized (mOperationsLock) {
+            for (int i = 0; i < mOperations.size(); i++) {
+                final Operation op = mOperations.valueAt(i);
+                final int token = mOperations.keyAt(i);
+                if (op.type == type) {
+                    tokens.add(token);
+                }
+            }
+            return tokens;
+        }
+    }
+
+    /** See {@link OperationStorage#operationTokensForOpState()} */
+    @Override
+    public Set<Integer> operationTokensForOpState(@OpState int state) {
+        Set<Integer> tokens = Sets.newHashSet();
+        synchronized (mOperationsLock) {
+            for (int i = 0; i < mOperations.size(); i++) {
+                final Operation op = mOperations.valueAt(i);
+                final int token = mOperations.keyAt(i);
+                if (op.state == state) {
+                    tokens.add(token);
+                }
+            }
+            return tokens;
+        }
+    }
+
+    /**
+     * A blocking function that blocks the caller until the operation identified
+     * by {@code token} is complete - either via a message from the backup,
+     * agent or through cancellation.
+     *
+     * @param token the operation token specified when registering the operation
+     * @param callback a lambda which is invoked once only when the operation
+     *                 completes - ie. if this method is called twice for the
+     *                 same token, the lambda is not invoked the second time.
+     * @return true if the operation was ACKed prior to or during this call.
+     */
+    public boolean waitUntilOperationComplete(int token, IntConsumer callback) {
+        if (MORE_DEBUG) {
+            Slog.i(TAG, "[UserID:" + mUserId + "] Blocking until operation complete for "
+                    + Integer.toHexString(token));
+        }
+        @OpState int finalState = OpState.PENDING;
+        Operation op = null;
+        synchronized (mOperationsLock) {
+            while (true) {
+                op = mOperations.get(token);
+                if (op == null) {
+                    // mysterious disappearance: treat as success with no callback
+                    break;
+                } else {
+                    if (op.state == OpState.PENDING) {
+                        try {
+                            mOperationsLock.wait();
+                        } catch (InterruptedException e) {
+                            Slog.w(TAG, "Waiting on mOperationsLock: ", e);
+                        }
+                        // When the wait is notified we loop around and recheck the current state
+                    } else {
+                        if (MORE_DEBUG) {
+                            Slog.d(TAG, "[UserID:" + mUserId
+                                    + "] Unblocked waiting for operation token="
+                                    + Integer.toHexString(token));
+                        }
+                        // No longer pending; we're done
+                        finalState = op.state;
+                        break;
+                    }
+                }
+            }
+        }
+
+        removeOperation(token);
+        if (op != null) {
+            callback.accept(op.type);
+        }
+        if (MORE_DEBUG) {
+            Slog.v(TAG, "[UserID:" + mUserId + "] operation " + Integer.toHexString(token)
+                    + " complete: finalState=" + finalState);
+        }
+        return finalState == OpState.ACKNOWLEDGED;
+    }
+
+    /**
+     * Signals that an ongoing operation is complete: after a currently-active
+     * backup agent has notified us that it has completed the outstanding
+     * asynchronous backup/restore operation identified by the supplied
+     * {@code} token.
+     *
+     * @param token the operation token specified when registering the operation
+     * @param result a result code or error code for the completed operation
+     * @param callback a lambda that is invoked if the completion moves the
+     *                 operation from PENDING to ACKNOWLEDGED state.
+     */
+    public void onOperationComplete(int token, long result, Consumer<BackupRestoreTask> callback) {
+        if (MORE_DEBUG) {
+            Slog.v(TAG, "[UserID:" + mUserId + "] onOperationComplete: "
+                    + Integer.toHexString(token) + " result=" + result);
+        }
+        Operation op = null;
+        synchronized (mOperationsLock) {
+            op = mOperations.get(token);
+            if (op != null) {
+                if (op.state == OpState.TIMEOUT) {
+                    // The operation already timed out, and this is a late response.  Tidy up
+                    // and ignore it; we've already dealt with the timeout.
+                    op = null;
+                    mOperations.remove(token);
+                } else if (op.state == OpState.ACKNOWLEDGED) {
+                    if (DEBUG) {
+                        Slog.w(TAG, "[UserID:" + mUserId + "] Received duplicate ack for token="
+                                + Integer.toHexString(token));
+                    }
+                    op = null;
+                    mOperations.remove(token);
+                } else if (op.state == OpState.PENDING) {
+                    // Can't delete op from mOperations. waitUntilOperationComplete can be
+                    // called after we we receive this call.
+                    op.state = OpState.ACKNOWLEDGED;
+                }
+            }
+            mOperationsLock.notifyAll();
+        }
+
+        // Invoke the operation's completion callback, if there is one.
+        if (op != null && op.callback != null) {
+            callback.accept(op.callback);
+        }
+    }
+
+    /**
+     * Cancel the operation associated with {@code token}.  Cancellation may be
+     * propagated to the operation's callback (a {@link BackupRestoreTask}) if
+     * the operation has one, and the cancellation is due to the operation
+     * timing out.
+     *
+     * @param token the operation token specified when registering the operation
+     * @param cancelAll this is passed on when propagating the cancellation
+     * @param operationTimedOutCallback a lambda that is invoked with the
+     *                                  operation type where the operation is
+     *                                  cancelled due to timeout, allowing the
+     *                                  caller to do type-specific clean-ups.
+     */
+    public void cancelOperation(
+            int token, boolean cancelAll, IntConsumer operationTimedOutCallback) {
+        // Notify any synchronous waiters
+        Operation op = null;
+        synchronized (mOperationsLock) {
+            op = mOperations.get(token);
+            if (MORE_DEBUG) {
+                if (op == null) {
+                    Slog.w(TAG, "[UserID:" + mUserId + "] Cancel of token "
+                            + Integer.toHexString(token) + " but no op found");
+                }
+            }
+            int state = (op != null) ? op.state : OpState.TIMEOUT;
+            if (state == OpState.ACKNOWLEDGED) {
+                // The operation finished cleanly, so we have nothing more to do.
+                if (DEBUG) {
+                    Slog.w(TAG, "[UserID:" + mUserId + "] Operation already got an ack."
+                            + "Should have been removed from mCurrentOperations.");
+                }
+                op = null;
+                mOperations.delete(token);
+            } else if (state == OpState.PENDING) {
+                if (DEBUG) {
+                    Slog.v(TAG, "[UserID:" + mUserId + "] Cancel: token="
+                            + Integer.toHexString(token));
+                }
+                op.state = OpState.TIMEOUT;
+                // Can't delete op from mOperations here. waitUntilOperationComplete may be
+                // called after we receive cancel here. We need this op's state there.
+                operationTimedOutCallback.accept(op.type);
+            }
+            mOperationsLock.notifyAll();
+        }
+
+        // If there's a TimeoutHandler for this event, call it
+        if (op != null && op.callback != null) {
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "[UserID:" + mUserId + "   Invoking cancel on " + op.callback);
+            }
+            op.callback.handleCancel(cancelAll);
+        }
+    }
+};
diff --git a/services/backup/java/com/android/server/backup/internal/PerformClearTask.java b/services/backup/java/com/android/server/backup/internal/PerformClearTask.java
index 80bd604..de0177c 100644
--- a/services/backup/java/com/android/server/backup/internal/PerformClearTask.java
+++ b/services/backup/java/com/android/server/backup/internal/PerformClearTask.java
@@ -21,9 +21,9 @@
 import android.content.pm.PackageInfo;
 import android.util.Slog;
 
-import com.android.internal.backup.IBackupTransport;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 
 import java.io.File;
@@ -47,7 +47,7 @@
 
     public void run() {
         String callerLogString = "PerformClearTask.run()";
-        IBackupTransport transport = null;
+        BackupTransportClient transport = null;
         try {
             // Clear the on-device backup state to ensure a full backup next time
             String transportDirName =
diff --git a/services/backup/java/com/android/server/backup/internal/PerformInitializeTask.java b/services/backup/java/com/android/server/backup/internal/PerformInitializeTask.java
index 7636ef6..888f49d 100644
--- a/services/backup/java/com/android/server/backup/internal/PerformInitializeTask.java
+++ b/services/backup/java/com/android/server/backup/internal/PerformInitializeTask.java
@@ -28,10 +28,10 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.backup.IBackupTransport;
 import com.android.server.EventLogTags;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 
 import java.io.File;
@@ -128,7 +128,8 @@
                 EventLog.writeEvent(EventLogTags.BACKUP_START, transportDirName);
                 long startRealtime = SystemClock.elapsedRealtime();
 
-                IBackupTransport transport = transportConnection.connectOrThrow(callerLogString);
+                BackupTransportClient transport = transportConnection.connectOrThrow(
+                        callerLogString);
                 int status = transport.initializeDevice();
                 if (status != BackupTransport.TRANSPORT_OK) {
                     Slog.e(TAG, "Transport error in initializeDevice()");
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
index bdb2e6f..30da8c1 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
@@ -51,7 +51,6 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.backup.IBackupTransport;
 import com.android.internal.util.Preconditions;
 import com.android.server.AppWidgetBackupBridge;
 import com.android.server.backup.BackupAgentTimeoutParameters;
@@ -65,6 +64,7 @@
 import com.android.server.backup.remote.RemoteCall;
 import com.android.server.backup.remote.RemoteCallable;
 import com.android.server.backup.remote.RemoteResult;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.transport.TransportNotAvailableException;
 import com.android.server.backup.utils.BackupEligibilityRules;
@@ -111,7 +111,7 @@
  * </ul>
  *
  * If there is no PackageManager (PM) pseudo-package state file in the state directory, the
- * specified transport will be initialized with {@link IBackupTransport#initializeDevice()}.
+ * specified transport will be initialized with {@link BackupTransportClient#initializeDevice()}.
  *
  * <p>The PM pseudo-package is the first package to be backed-up and sent to the transport in case
  * of incremental choice. If non-incremental, PM will only be backed-up if specified in the queue,
@@ -141,8 +141,8 @@
  *       </ul>
  *   <li>Unbind the agent.
  *   <li>Assuming agent response, send the staged data that the agent wrote to disk to the transport
- *       via {@link IBackupTransport#performBackup(PackageInfo, ParcelFileDescriptor, int)}.
- *   <li>Call {@link IBackupTransport#finishBackup()} if previous call was successful.
+ *       via {@link BackupTransportClient#performBackup(PackageInfo, ParcelFileDescriptor, int)}.
+ *   <li>Call {@link BackupTransportClient#finishBackup()} if previous call was successful.
  *   <li>Save the new state in the state file. During the agent call it was being written to
  *       &lt;state file&gt;.new, here we rename it and replace the old one.
  *   <li>Delete the stage file.
@@ -155,7 +155,7 @@
  *   <li>Delete the {@link DataChangedJournal} provided. Note that this should not be the current
  *       journal.
  *   <li>Set {@link UserBackupManagerService} current token as {@link
- *       IBackupTransport#getCurrentRestoreSet()}, if applicable.
+ *       BackupTransportClient#getCurrentRestoreSet()}, if applicable.
  *   <li>Add the transport to the list of transports pending initialization ({@link
  *       UserBackupManagerService#getPendingInits()}) and kick-off initialization if the transport
  *       ever returned {@link BackupTransport#TRANSPORT_NOT_INITIALIZED}.
@@ -194,7 +194,7 @@
      * @param backupManagerService The {@link UserBackupManagerService} instance.
      * @param transportConnection The {@link TransportConnection} that contains the transport used
      *     for the operation.
-     * @param transportDirName The value of {@link IBackupTransport#transportDirName()} for the
+     * @param transportDirName The value of {@link BackupTransportClient#transportDirName()} for the
      *     transport whose {@link TransportConnection} was provided above.
      * @param queue The list of package names that will be backed-up.
      * @param dataChangedJournal The old data-changed journal file that will be deleted when the
@@ -417,7 +417,7 @@
 
         boolean noDataPackageEncountered = false;
         try {
-            IBackupTransport transport =
+            BackupTransportClient transport =
                     mTransportConnection.connectOrThrow("KVBT.informTransportOfEmptyBackups()");
 
             for (String packageName : succeedingPackages) {
@@ -467,8 +467,8 @@
     }
 
     /** Send the "no data changed" message to a transport for a specific package */
-    private void sendNoDataChangedTo(IBackupTransport transport, PackageInfo packageInfo, int flags)
-            throws RemoteException {
+    private void sendNoDataChangedTo(BackupTransportClient transport, PackageInfo packageInfo,
+            int flags) throws RemoteException {
         ParcelFileDescriptor pfd;
         try {
             pfd = ParcelFileDescriptor.open(mBlankStateFile, MODE_READ_ONLY | MODE_CREATE);
@@ -608,7 +608,8 @@
         mReporter.onQueueReady(mQueue);
         File pmState = new File(mStateDirectory, PM_PACKAGE);
         try {
-            IBackupTransport transport = mTransportConnection.connectOrThrow("KVBT.startTask()");
+            BackupTransportClient transport = mTransportConnection.connectOrThrow(
+                    "KVBT.startTask()");
             String transportName = transport.name();
             if (transportName.contains("EncryptedLocalTransport")) {
                 // Temporary code for EiTF POC. Only supports non-incremental backups.
@@ -764,7 +765,8 @@
         long currentToken = mBackupManagerService.getCurrentToken();
         if (mHasDataToBackup && (status == BackupTransport.TRANSPORT_OK) && (currentToken == 0)) {
             try {
-                IBackupTransport transport = mTransportConnection.connectOrThrow(callerLogString);
+                BackupTransportClient transport = mTransportConnection.connectOrThrow(
+                        callerLogString);
                 transportName = transport.name();
                 mBackupManagerService.setCurrentToken(transport.getCurrentRestoreSet());
                 mBackupManagerService.writeRestoreTokens();
@@ -835,7 +837,7 @@
 
     @GuardedBy("mQueueLock")
     private void triggerTransportInitializationLocked() throws Exception {
-        IBackupTransport transport =
+        BackupTransportClient transport =
                 mTransportConnection.connectOrThrow("KVBT.triggerTransportInitializationLocked");
         mBackupManagerService.getPendingInits().add(transport.name());
         deletePmStateFile();
@@ -919,7 +921,7 @@
                 }
             }
 
-            IBackupTransport transport = mTransportConnection.connectOrThrow(
+            BackupTransportClient transport = mTransportConnection.connectOrThrow(
                     "KVBT.extractAgentData()");
             long quota = transport.getBackupQuota(packageName, /* isFullBackup */ false);
             int transportFlags = transport.getTransportFlags();
@@ -1078,7 +1080,7 @@
         int status;
         try (ParcelFileDescriptor backupData =
                 ParcelFileDescriptor.open(backupDataFile, MODE_READ_ONLY)) {
-            IBackupTransport transport =
+            BackupTransportClient transport =
                     mTransportConnection.connectOrThrow("KVBT.transportPerformBackup()");
             mReporter.onTransportPerformBackup(packageName);
             int flags = getPerformBackupFlags(mUserInitiated, nonIncremental);
@@ -1131,7 +1133,7 @@
     private void agentDoQuotaExceeded(@Nullable IBackupAgent agent, String packageName, long size) {
         if (agent != null) {
             try {
-                IBackupTransport transport =
+                BackupTransportClient transport =
                         mTransportConnection.connectOrThrow("KVBT.agentDoQuotaExceeded()");
                 long quota = transport.getBackupQuota(packageName, false);
                 remoteCall(
@@ -1227,7 +1229,7 @@
         mReporter.onRevertTask();
         long delay;
         try {
-            IBackupTransport transport =
+            BackupTransportClient transport =
                     mTransportConnection.connectOrThrow("KVBT.revertTask()");
             delay = transport.requestBackupTime();
         } catch (Exception e) {
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 8c786d5..ac831af 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -54,7 +54,6 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.backup.IBackupTransport;
 import com.android.server.AppWidgetBackupBridge;
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
@@ -66,6 +65,7 @@
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.utils.BackupEligibilityRules;
 import com.android.server.backup.utils.BackupManagerMonitorUtils;
@@ -397,7 +397,7 @@
 
             PackageInfo[] packages = mAcceptSet.toArray(new PackageInfo[0]);
 
-            IBackupTransport transport =
+            BackupTransportClient transport =
                     mTransportConnection.connectOrThrow("PerformUnifiedRestoreTask.startRestore()");
 
             mStatus = transport.startRestore(mToken, packages);
@@ -495,7 +495,7 @@
     private void dispatchNextRestore() {
         UnifiedRestoreState nextState = UnifiedRestoreState.FINAL;
         try {
-            IBackupTransport transport =
+            BackupTransportClient transport =
                     mTransportConnection.connectOrThrow(
                             "PerformUnifiedRestoreTask.dispatchNextRestore()");
             mRestoreDescription = transport.nextRestorePackage();
@@ -709,7 +709,7 @@
         boolean startedAgentRestore = false;
 
         try {
-            IBackupTransport transport =
+            BackupTransportClient transport =
                     mTransportConnection.connectOrThrow(
                             "PerformUnifiedRestoreTask.initiateOneRestore()");
 
@@ -940,7 +940,8 @@
 
             String callerLogString = "PerformUnifiedRestoreTask$StreamFeederThread.run()";
             try {
-                IBackupTransport transport = mTransportConnection.connectOrThrow(callerLogString);
+                BackupTransportClient transport = mTransportConnection.connectOrThrow(
+                        callerLogString);
                 while (status == BackupTransport.TRANSPORT_OK) {
                     // have the transport write some of the restoring data to us
                     int result = transport.getNextFullRestoreDataChunk(tWriteEnd);
@@ -1032,7 +1033,7 @@
                     // Something went wrong somewhere.  Whether it was at the transport
                     // level is immaterial; we need to tell the transport to bail
                     try {
-                        IBackupTransport transport =
+                        BackupTransportClient transport =
                                 mTransportConnection.connectOrThrow(callerLogString);
                         transport.abortFullRestore();
                     } catch (Exception e) {
@@ -1095,7 +1096,7 @@
 
         String callerLogString = "PerformUnifiedRestoreTask.finalizeRestore()";
         try {
-            IBackupTransport transport =
+            BackupTransportClient transport =
                     mTransportConnection.connectOrThrow(callerLogString);
             transport.finishRestore();
         } catch (Exception e) {
diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
index 652386f..bd1ac2dc 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
@@ -40,8 +40,8 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.backup.IBackupTransport;
 import com.android.internal.util.ArrayUtils;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 
 import com.google.android.collect.Sets;
@@ -237,7 +237,7 @@
             }
             if (transportConnection != null) {
                 try {
-                    IBackupTransport transport =
+                    BackupTransportClient transport =
                             transportConnection.connectOrThrow(
                                     "AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport");
                     return transport.isAppEligibleForBackup(
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index a0a00f7..bcc345f 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -20,7 +20,7 @@
 import static com.android.internal.util.FunctionalUtils.uncheckExceptions;
 import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
 import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG;
-import static com.android.server.companion.PermissionsUtils.enforceCallerPermissionsToRequest;
+import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation;
 import static com.android.server.companion.RolesUtils.isRoleHolder;
 
 import static java.util.Objects.requireNonNull;
@@ -110,7 +110,7 @@
         }
 
         // 1. Enforce permissions and other requirements.
-        enforceCallerPermissionsToRequest(mContext, request, packageName, userId);
+        enforcePermissionsForAssociation(mContext, request, packageName, userId);
         mService.checkUsesFeature(packageName, userId);
 
         // 2. Check if association can be created without launching UI (i.e. CDM needs NEITHER
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index e0cd472..b32d543 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -39,7 +39,8 @@
 import static com.android.server.companion.PermissionsUtils.checkCallerCanManageAssociationsForPackage;
 import static com.android.server.companion.PermissionsUtils.checkCallerCanManageCompanionDevice;
 import static com.android.server.companion.PermissionsUtils.enforceCallerCanInteractWithUserId;
-import static com.android.server.companion.PermissionsUtils.enforceCallerCanManagerCompanionDevice;
+import static com.android.server.companion.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
+import static com.android.server.companion.PermissionsUtils.enforceCallerCanManageCompanionDevice;
 import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOr;
 import static com.android.server.companion.RolesUtils.addRoleHolderForAssociation;
 import static com.android.server.companion.RolesUtils.removeRoleHolderForAssociation;
@@ -401,15 +402,16 @@
             Slog.i(LOG_TAG, "associate() "
                     + "request=" + request + ", "
                     + "package=u" + userId + "/" + packageName);
+            enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
+                    "create associations");
+
             mAssociationRequestsProcessor.process(request, packageName, userId, callback);
         }
 
         @Override
         public List<AssociationInfo> getAssociations(String packageName, int userId) {
-            if (!checkCallerCanManageAssociationsForPackage(getContext(), userId, packageName)) {
-                throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have "
-                        + "permissions to get associations for u" + userId + "/" + packageName);
-            }
+            enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
+                    "get associations");
 
             if (!checkCallerCanManageCompanionDevice(getContext())) {
                 // If the caller neither is system nor holds MANAGE_COMPANION_DEVICES: it needs to
@@ -424,7 +426,7 @@
         @Override
         public List<AssociationInfo> getAllAssociationsForUser(int userId) throws RemoteException {
             enforceCallerCanInteractWithUserId(getContext(), userId);
-            enforceCallerCanManagerCompanionDevice(getContext(), "getAllAssociationsForUser");
+            enforceCallerCanManageCompanionDevice(getContext(), "getAllAssociationsForUser");
 
             return new ArrayList<>(
                     CompanionDeviceManagerService.this.getAllAssociationsForUser(userId));
@@ -434,7 +436,7 @@
         public void addOnAssociationsChangedListener(IOnAssociationsChangedListener listener,
                 int userId) {
             enforceCallerCanInteractWithUserId(getContext(), userId);
-            enforceCallerCanManagerCompanionDevice(getContext(),
+            enforceCallerCanManageCompanionDevice(getContext(),
                     "addOnAssociationsChangedListener");
 
             //TODO: Implement.
@@ -621,7 +623,7 @@
         public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
                 String[] args, ShellCallback callback, ResultReceiver resultReceiver)
                 throws RemoteException {
-            enforceCallerCanManagerCompanionDevice(getContext(), "onShellCommand");
+            enforceCallerCanManageCompanionDevice(getContext(), "onShellCommand");
             new CompanionDeviceShellCommand(CompanionDeviceManagerService.this)
                     .exec(this, in, out, err, args, callback, resultReceiver);
         }
diff --git a/services/companion/java/com/android/server/companion/PermissionsUtils.java b/services/companion/java/com/android/server/companion/PermissionsUtils.java
index 45097f0..ea57089 100644
--- a/services/companion/java/com/android/server/companion/PermissionsUtils.java
+++ b/services/companion/java/com/android/server/companion/PermissionsUtils.java
@@ -24,6 +24,7 @@
 import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
 import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.Binder.getCallingPid;
 import static android.os.Binder.getCallingUid;
 import static android.os.Process.SYSTEM_UID;
 import static android.os.UserHandle.getCallingUserId;
@@ -37,11 +38,13 @@
 import android.companion.AssociationRequest;
 import android.companion.CompanionDeviceManager;
 import android.content.Context;
+import android.content.pm.PackageManagerInternal;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.ArrayMap;
 
 import com.android.internal.app.IAppOpsService;
+import com.android.server.LocalServices;
 
 import java.util.Map;
 
@@ -65,21 +68,19 @@
         DEVICE_PROFILE_TO_PERMISSION = unmodifiableMap(map);
     }
 
-    static void enforceCallerPermissionsToRequest(@NonNull Context context,
+    static void enforcePermissionsForAssociation(@NonNull Context context,
             @NonNull AssociationRequest request, @NonNull String packageName,
             @UserIdInt int userId) {
-        enforceCallerCanInteractWithUserId(context, userId);
-        enforceCallerIsSystemOr(userId, packageName);
-
-        enforceRequestDeviceProfilePermissions(context, request.getDeviceProfile());
+        final int packageUid = getPackageUid(userId, packageName);
+        enforceRequestDeviceProfilePermissions(context, request.getDeviceProfile(), packageUid);
 
         if (request.isSelfManaged()) {
-            enforceRequestSelfManagedPermission(context);
+            enforceRequestSelfManagedPermission(context, packageUid);
         }
     }
 
     static void enforceRequestDeviceProfilePermissions(
-            @NonNull Context context, @Nullable String deviceProfile) {
+            @NonNull Context context, @Nullable String deviceProfile, int packageUid) {
         // Device profile can be null.
         if (deviceProfile == null) return;
 
@@ -100,14 +101,15 @@
         }
 
         final String permission = DEVICE_PROFILE_TO_PERMISSION.get(deviceProfile);
-        if (context.checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
+        if (context.checkPermission(permission, getCallingPid(), packageUid)
+                != PERMISSION_GRANTED) {
             throw new SecurityException("Application must hold " + permission + " to associate "
                     + "with a device with " + deviceProfile + " profile.");
         }
     }
 
-    static void enforceRequestSelfManagedPermission(@NonNull Context context) {
-        if (context.checkCallingOrSelfPermission(REQUEST_COMPANION_SELF_MANAGED)
+    static void enforceRequestSelfManagedPermission(@NonNull Context context, int packageUid) {
+        if (context.checkPermission(REQUEST_COMPANION_SELF_MANAGED, getCallingPid(), packageUid)
                 != PERMISSION_GRANTED) {
             throw new SecurityException("Application does not hold "
                     + REQUEST_COMPANION_SELF_MANAGED);
@@ -159,13 +161,34 @@
         return context.checkCallingPermission(MANAGE_COMPANION_DEVICES) == PERMISSION_GRANTED;
     }
 
-    static void enforceCallerCanManagerCompanionDevice(@NonNull Context context,
+    static void enforceCallerCanManageCompanionDevice(@NonNull Context context,
             @Nullable String message) {
         if (getCallingUid() == SYSTEM_UID) return;
 
         context.enforceCallingPermission(MANAGE_COMPANION_DEVICES, message);
     }
 
+    static void enforceCallerCanManageAssociationsForPackage(@NonNull Context context,
+            @UserIdInt int userId, @NonNull String packageName,
+            @Nullable String actionDescription) {
+        if (checkCallerCanManageAssociationsForPackage(context, userId, packageName)) return;
+
+        throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have "
+                + "permissions to "
+                + (actionDescription != null ? actionDescription : "manage associations")
+                + " for u" + userId + "/" + packageName);
+    }
+
+    /**
+     * Check if the caller is either:
+     * <ul>
+     * <li> the package itself
+     * <li> the System ({@link android.os.Process#SYSTEM_UID})
+     * <li> holds {@link Manifest.permission#MANAGE_COMPANION_DEVICES} and, if belongs to a
+     * different user, also holds {@link Manifest.permission#INTERACT_ACROSS_USERS}.
+     * </ul>
+     * @return whether the caller is one of the above.
+     */
     static boolean checkCallerCanManageAssociationsForPackage(@NonNull Context context,
             @UserIdInt int userId, @NonNull String packageName) {
         if (checkCallerIsSystemOr(userId, packageName)) return true;
@@ -184,6 +207,11 @@
         }
     }
 
+    private static int getPackageUid(@UserIdInt int userId, @NonNull String packageName) {
+        return LocalServices.getService(PackageManagerInternal.class)
+                .getPackageUid(packageName, 0, userId);
+    }
+
     private static IAppOpsService getAppOpsService() {
         if (sAppOpsService == null) {
             synchronized (PermissionsUtils.class) {
diff --git a/services/core/java/com/android/server/OWNERS b/services/core/java/com/android/server/OWNERS
index b641377..71b463a 100644
--- a/services/core/java/com/android/server/OWNERS
+++ b/services/core/java/com/android/server/OWNERS
@@ -25,13 +25,14 @@
 per-file *Binder* = file:/core/java/com/android/internal/os/BINDER_OWNERS
 per-file *Bluetooth* = file:/core/java/android/bluetooth/OWNERS
 per-file *Gnss* = file:/services/core/java/com/android/server/location/OWNERS
+per-file **IpSec* = file:/services/core/java/com/android/server/net/OWNERS
+per-file **IpSec* = file:/services/core/java/com/android/server/vcn/OWNERS
 per-file *Location* = file:/services/core/java/com/android/server/location/OWNERS
 per-file *Network* = file:/services/core/java/com/android/server/net/OWNERS
 per-file *Storage* = file:/core/java/android/os/storage/OWNERS
 per-file *TimeUpdate* = file:/core/java/android/app/timezone/OWNERS
 per-file DynamicSystemService.java = file:/packages/DynamicSystemInstallationService/OWNERS
 per-file GestureLauncherService.java = file:platform/packages/apps/EmergencyInfo:/OWNERS
-per-file IpSecService.java = file:/services/core/java/com/android/server/net/OWNERS
 per-file MmsServiceBroker.java = file:/telephony/OWNERS
 per-file NetIdManager.java = file:/services/core/java/com/android/server/net/OWNERS
 per-file PackageWatchdog.java, RescueParty.java = file:/services/core/java/com/android/server/rollback/OWNERS
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 80a8d63..1a89ae7 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2271,7 +2271,8 @@
                 // not the calling one.
                 appInfo.packageName = app.getHostingRecord().getDefiningPackageName();
                 appInfo.uid = uid;
-                appZygote = new AppZygote(appInfo, uid, firstUid, lastUid);
+                int runtimeFlags = decideTaggingLevel(app);
+                appZygote = new AppZygote(appInfo, uid, firstUid, lastUid, runtimeFlags);
                 mAppZygotes.put(app.info.processName, uid, appZygote);
                 zygoteProcessList = new ArrayList<ProcessRecord>();
                 mAppZygoteProcesses.put(appZygote, zygoteProcessList);
diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java
index b9cc992..8de515d 100644
--- a/services/core/java/com/android/server/appop/DiscreteRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java
@@ -134,7 +134,7 @@
     private static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
             + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," + OP_PHONE_CALL_MICROPHONE + ","
             + OP_PHONE_CALL_CAMERA;
-    private static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofHours(24).toMillis();
+    private static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis();
     private static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis();
     private static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION =
             Duration.ofMinutes(1).toMillis();
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index ff451a3..e4ac7be 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -493,6 +493,12 @@
         return isDeviceActiveForCommunication(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
     }
 
+    /*package*/ boolean isDeviceConnected(@NonNull AudioDeviceAttributes device) {
+        synchronized (mDeviceStateLock) {
+            return mDeviceInventory.isDeviceConnected(device);
+        }
+    }
+
     /*package*/ void setWiredDeviceConnectionState(int type,
             @AudioService.ConnectionState int state, String address, String name,
             String caller) {
@@ -502,6 +508,13 @@
         }
     }
 
+    /*package*/ void setTestDeviceConnectionState(@NonNull AudioDeviceAttributes device,
+            @AudioService.ConnectionState int state) {
+        synchronized (mDeviceStateLock) {
+            mDeviceInventory.setTestDeviceConnectionState(device, state);
+        }
+    }
+
     /*package*/ static final class BleVolumeInfo {
         final int mIndex;
         final int mMaxIndex;
@@ -1002,7 +1015,8 @@
     /*package*/ boolean handleDeviceConnection(boolean connect, int device, String address,
                                                        String deviceName) {
         synchronized (mDeviceStateLock) {
-            return mDeviceInventory.handleDeviceConnection(connect, device, address, deviceName);
+            return mDeviceInventory.handleDeviceConnection(connect, device, address, deviceName,
+                    false /*for test*/);
         }
     }
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index f32d3b5..a27e4b77 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -224,6 +224,7 @@
         public final String mAddress;
         public final String mName;
         public final String mCaller;
+        public boolean mForTest = false;
 
         /*package*/ WiredDeviceConnectionState(int type, @AudioService.ConnectionState int state,
                                                String address, String name, String caller) {
@@ -521,7 +522,7 @@
             }
 
             if (!handleDeviceConnection(wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED,
-                    wdcs.mType, wdcs.mAddress, wdcs.mName)) {
+                    wdcs.mType, wdcs.mAddress, wdcs.mName, wdcs.mForTest)) {
                 // change of connection state failed, bailout
                 mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed")
                         .record();
@@ -593,7 +594,7 @@
     }
 
     //------------------------------------------------------------
-    //
+    // preferred device(s)
 
     /*package*/ int setPreferredDevicesForStrategySync(int strategy,
             @NonNull List<AudioDeviceAttributes> devices) {
@@ -674,16 +675,34 @@
         mDevRoleCapturePresetDispatchers.unregister(dispatcher);
     }
 
+    //-----------------------------------------------------------------------
+
+    /**
+     * Check if a device is in the list of connected devices
+     * @param device the device whose connection state is queried
+     * @return true if connected
+     */
+    // called with AudioDeviceBroker.mDeviceStateLock lock held
+    public boolean isDeviceConnected(@NonNull AudioDeviceAttributes device) {
+        final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(),
+                device.getAddress());
+        synchronized (mDevicesLock) {
+            return (mConnectedDevices.get(key) != null);
+        }
+    }
+
     /**
      * Implements the communication with AudioSystem to (dis)connect a device in the native layers
      * @param connect true if connection
      * @param device the device type
      * @param address the address of the device
      * @param deviceName human-readable name of device
+     * @param isForTesting if true, not calling AudioSystem for the connection as this is
+     *                    just for testing
      * @return false if an error was reported by AudioSystem
      */
     /*package*/ boolean handleDeviceConnection(boolean connect, int device, String address,
-            String deviceName) {
+            String deviceName, boolean isForTesting) {
         if (AudioService.DEBUG_DEVICES) {
             Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:"
                     + Integer.toHexString(device) + " address:" + address
@@ -706,9 +725,14 @@
                 Slog.i(TAG, "deviceInfo:" + di + " is(already)Connected:" + isConnected);
             }
             if (connect && !isConnected) {
-                final int res = mAudioSystem.setDeviceConnectionState(device,
-                        AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName,
-                        AudioSystem.AUDIO_FORMAT_DEFAULT);
+                final int res;
+                if (isForTesting) {
+                    res = AudioSystem.AUDIO_STATUS_OK;
+                } else {
+                    res = mAudioSystem.setDeviceConnectionState(device,
+                            AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName,
+                            AudioSystem.AUDIO_FORMAT_DEFAULT);
+                }
                 if (res != AudioSystem.AUDIO_STATUS_OK) {
                     final String reason = "not connecting device 0x" + Integer.toHexString(device)
                             + " due to command error " + res;
@@ -914,6 +938,15 @@
         }
     }
 
+    /*package*/ void setTestDeviceConnectionState(@NonNull AudioDeviceAttributes device,
+            @AudioService.ConnectionState int state) {
+        final WiredDeviceConnectionState connection = new WiredDeviceConnectionState(
+                device.getInternalType(), state, device.getAddress(),
+                "test device", "com.android.server.audio");
+        connection.mForTest = true;
+        onSetWiredDeviceConnectionState(connection);
+    }
+
     //-------------------------------------------------------------------
     // Internal utilities
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 9c8a663..aa33644 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -35,6 +35,7 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
@@ -92,6 +93,7 @@
 import android.media.IAudioService;
 import android.media.ICapturePresetDevicesRoleDispatcher;
 import android.media.ICommunicationDeviceDispatcher;
+import android.media.IMuteAwaitConnectionCallback;
 import android.media.IPlaybackConfigDispatcher;
 import android.media.IRecordingConfigDispatcher;
 import android.media.IRingtonePlayer;
@@ -1027,7 +1029,8 @@
         readUserRestrictions();
 
         mPlaybackMonitor =
-                new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM]);
+                new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM],
+                        device -> onMuteAwaitConnectionTimeout(device));
         mPlaybackMonitor.registerPlaybackCallback(mVoicePlaybackActivityMonitor, true);
 
         mMediaFocusControl = new MediaFocusControl(mContext, mPlaybackMonitor);
@@ -1048,6 +1051,9 @@
 
         mHasSpatializerEffect = SystemProperties.getBoolean("ro.audio.spatializer_enabled", false);
 
+        // monitor routing updates coming from native
+        mAudioSystem.setRoutingListener(this);
+
         // done with service initialization, continue additional work in our Handler thread
         queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_STREAMS_VOLUMES,
                 0 /* arg1 */,  0 /* arg2 */, null /* obj */,  0 /* delay */);
@@ -1251,20 +1257,22 @@
     // routing monitoring from AudioSystemAdapter
     @Override
     public void onRoutingUpdatedFromNative() {
-        if (!mHasSpatializerEffect) {
-            return;
-        }
         sendMsg(mAudioHandler,
                 MSG_ROUTING_UPDATED,
                 SENDMSG_REPLACE, 0, 0, null,
                 /*delay*/ 0);
     }
 
-    void monitorRoutingChanges(boolean enabled) {
-        mAudioSystem.setRoutingListener(enabled ? this : null);
+    /**
+     * called when handling MSG_ROUTING_UPDATED
+     */
+    void onRoutingUpdatedFromAudioThread() {
+        if (mHasSpatializerEffect) {
+            mSpatializerHelper.onRoutingUpdated();
+        }
+        checkMuteAwaitConnection();
     }
 
-
     //-----------------------------------------------------------------
     RoleObserver mRoleObserver;
 
@@ -1452,7 +1460,6 @@
 
         if (mHasSpatializerEffect) {
             mSpatializerHelper.reset(/* featureEnabled */ isSpatialAudioEnabled());
-            monitorRoutingChanges(true);
         }
 
         onIndicateSystemReady();
@@ -6357,6 +6364,20 @@
         mDeviceBroker.setWiredDeviceConnectionState(type, state, address, name, caller);
     }
 
+    /** @see AudioManager#setTestDeviceConnectionState(AudioDeviceAttributes, boolean) */
+    public void setTestDeviceConnectionState(@NonNull AudioDeviceAttributes device,
+            boolean connected) {
+        Objects.requireNonNull(device);
+        enforceModifyAudioRoutingPermission();
+        mDeviceBroker.setTestDeviceConnectionState(device,
+                connected ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED);
+        // simulate a routing update from native
+        sendMsg(mAudioHandler,
+                MSG_ROUTING_UPDATED,
+                SENDMSG_REPLACE, 0, 0, null,
+                /*delay*/ 0);
+    }
+
     /**
      * @hide
      * The states that can be used with AudioService.setBluetoothHearingAidDeviceConnectionState()
@@ -7652,7 +7673,6 @@
                     mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect);
                     if (mHasSpatializerEffect) {
                         mSpatializerHelper.setFeatureEnabled(isSpatialAudioEnabled());
-                        monitorRoutingChanges(true);
                     }
                     mAudioEventWakeLock.release();
                     break;
@@ -7791,7 +7811,7 @@
                     break;
 
                 case MSG_ROUTING_UPDATED:
-                    mSpatializerHelper.onRoutingUpdated();
+                    onRoutingUpdatedFromAudioThread();
                     break;
 
                 case MSG_PERSIST_SPATIAL_AUDIO_ENABLED:
@@ -8612,6 +8632,171 @@
     }
 
     //==========================================================================================
+    private final Object mMuteAwaitConnectionLock = new Object();
+
+    /**
+     * The device that is expected to be connected soon, and causes players to be muted until
+     * its connection, or it times out.
+     * Null when no active muting command, or it has timed out.
+     */
+    @GuardedBy("mMuteAwaitConnectionLock")
+    private AudioDeviceAttributes mMutingExpectedDevice;
+    @GuardedBy("mMuteAwaitConnectionLock")
+    private @Nullable int[] mMutedUsagesAwaitingConnection;
+
+    /** @see AudioManager#muteAwaitConnection */
+    @SuppressLint("EmptyCatch") // callback exception caught inside dispatchMuteAwaitConnection
+    public void muteAwaitConnection(@NonNull int[] usages,
+            @NonNull AudioDeviceAttributes device, long timeOutMs) {
+        Objects.requireNonNull(usages);
+        Objects.requireNonNull(device);
+        enforceModifyAudioRoutingPermission();
+        if (timeOutMs <= 0 || usages.length == 0) {
+            throw new IllegalArgumentException("Invalid timeOutMs/usagesToMute");
+        }
+
+        if (mDeviceBroker.isDeviceConnected(device)) {
+            // not throwing an exception as there could be a race between a connection (server-side,
+            // notification of connection in flight) and a mute operation (client-side)
+            Log.i(TAG, "muteAwaitConnection ignored, device (" + device + ") already connected");
+            return;
+        }
+        synchronized (mMuteAwaitConnectionLock) {
+            if (mMutingExpectedDevice != null) {
+                Log.e(TAG, "muteAwaitConnection ignored, another in progress for device:"
+                        + mMutingExpectedDevice);
+                throw new IllegalStateException("muteAwaitConnection already in progress");
+            }
+            mMutingExpectedDevice = device;
+            mMutedUsagesAwaitingConnection = usages;
+            mPlaybackMonitor.muteAwaitConnection(usages, device, timeOutMs);
+        }
+        dispatchMuteAwaitConnection(cb -> { try {
+            cb.dispatchOnMutedUntilConnection(device, usages); } catch (RemoteException e) { } });
+    }
+
+    /** @see AudioManager#getMutingExpectedDevice */
+    public @Nullable AudioDeviceAttributes getMutingExpectedDevice() {
+        enforceModifyAudioRoutingPermission();
+        synchronized (mMuteAwaitConnectionLock) {
+            return mMutingExpectedDevice;
+        }
+    }
+
+    /** @see AudioManager#cancelMuteAwaitConnection */
+    @SuppressLint("EmptyCatch") // callback exception caught inside dispatchMuteAwaitConnection
+    public void cancelMuteAwaitConnection(@NonNull AudioDeviceAttributes device) {
+        Objects.requireNonNull(device);
+        enforceModifyAudioRoutingPermission();
+        Log.i(TAG, "cancelMuteAwaitConnection for device:" + device);
+        final int[] mutedUsages;
+        synchronized (mMuteAwaitConnectionLock) {
+            if (mMutingExpectedDevice == null) {
+                // not throwing an exception as there could be a race between a timeout
+                // (server-side) and a cancel operation (client-side)
+                Log.i(TAG, "cancelMuteAwaitConnection ignored, no expected device");
+                return;
+            }
+            if (!device.equals(mMutingExpectedDevice)) {
+                Log.e(TAG, "cancelMuteAwaitConnection ignored, got " + device
+                        + "] but expected device is" + mMutingExpectedDevice);
+                throw new IllegalStateException("cancelMuteAwaitConnection for wrong device");
+            }
+            mutedUsages = mMutedUsagesAwaitingConnection;
+            mMutingExpectedDevice = null;
+            mMutedUsagesAwaitingConnection = null;
+            mPlaybackMonitor.cancelMuteAwaitConnection();
+        }
+        dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent(
+                    AudioManager.MuteAwaitConnectionCallback.EVENT_CANCEL, device, mutedUsages);
+            } catch (RemoteException e) { } });
+    }
+
+    final RemoteCallbackList<IMuteAwaitConnectionCallback> mMuteAwaitConnectionDispatchers =
+            new RemoteCallbackList<IMuteAwaitConnectionCallback>();
+
+    /** @see AudioManager#registerMuteAwaitConnectionCallback */
+    public void registerMuteAwaitConnectionDispatcher(@NonNull IMuteAwaitConnectionCallback cb,
+            boolean register) {
+        enforceModifyAudioRoutingPermission();
+        if (register) {
+            mMuteAwaitConnectionDispatchers.register(cb);
+        } else {
+            mMuteAwaitConnectionDispatchers.unregister(cb);
+        }
+    }
+
+    @SuppressLint("EmptyCatch") // callback exception caught inside dispatchMuteAwaitConnection
+    void checkMuteAwaitConnection() {
+        final AudioDeviceAttributes device;
+        final int[] mutedUsages;
+        synchronized (mMuteAwaitConnectionLock) {
+            if (mMutingExpectedDevice == null) {
+                return;
+            }
+            device = mMutingExpectedDevice;
+            mutedUsages = mMutedUsagesAwaitingConnection;
+            if (!mDeviceBroker.isDeviceConnected(device)) {
+                return;
+            }
+            mMutingExpectedDevice = null;
+            mMutedUsagesAwaitingConnection = null;
+            Log.i(TAG, "muteAwaitConnection device " + device + " connected, unmuting");
+            mPlaybackMonitor.cancelMuteAwaitConnection();
+        }
+        dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent(
+                AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION, device, mutedUsages);
+            } catch (RemoteException e) { } });
+    }
+
+    /**
+     * Called by PlaybackActivityMonitor when the timeout hit for the mute on device connection
+     */
+    @SuppressLint("EmptyCatch") // callback exception caught inside dispatchMuteAwaitConnection
+    void onMuteAwaitConnectionTimeout(@NonNull AudioDeviceAttributes timedOutDevice) {
+        final int[] mutedUsages;
+        synchronized (mMuteAwaitConnectionLock) {
+            if (!timedOutDevice.equals(mMutingExpectedDevice)) {
+                return;
+            }
+            Log.i(TAG, "muteAwaitConnection timeout, clearing expected device "
+                    + mMutingExpectedDevice);
+            mutedUsages = mMutedUsagesAwaitingConnection;
+            mMutingExpectedDevice = null;
+            mMutedUsagesAwaitingConnection = null;
+        }
+        dispatchMuteAwaitConnection(cb -> { try {
+                cb.dispatchOnUnmutedEvent(
+                        AudioManager.MuteAwaitConnectionCallback.EVENT_TIMEOUT,
+                        timedOutDevice, mutedUsages);
+            } catch (RemoteException e) { } });
+    }
+
+    private void dispatchMuteAwaitConnection(
+            java.util.function.Consumer<IMuteAwaitConnectionCallback> callback) {
+        final int nbDispatchers = mMuteAwaitConnectionDispatchers.beginBroadcast();
+        // lazy initialization as errors unlikely
+        ArrayList<IMuteAwaitConnectionCallback> errorList = null;
+        for (int i = 0; i < nbDispatchers; i++) {
+            try {
+                callback.accept(mMuteAwaitConnectionDispatchers.getBroadcastItem(i));
+            } catch (Exception e) {
+                if (errorList == null) {
+                    errorList = new ArrayList<>(1);
+                }
+                errorList.add(mMuteAwaitConnectionDispatchers.getBroadcastItem(i));
+            }
+        }
+        if (errorList != null) {
+            for (IMuteAwaitConnectionCallback errorItem : errorList) {
+                mMuteAwaitConnectionDispatchers.unregister(errorItem);
+            }
+        }
+        mMuteAwaitConnectionDispatchers.finishBroadcast();
+    }
+
+
+    //==========================================================================================
     // Device orientation
     //==========================================================================================
     /**
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index b94cea4..b333ed2 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -17,9 +17,11 @@
 package com.android.server.audio;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
 import android.media.AudioSystem;
@@ -27,21 +29,27 @@
 import android.media.PlayerBase;
 import android.media.VolumeShaper;
 import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Message;
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 
 import java.io.PrintWriter;
 import java.text.DateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * Class to receive and dispatch updates from AudioSystem about recording configurations.
@@ -54,6 +62,7 @@
     /*package*/ static final boolean DEBUG = false;
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1;
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_FADEOUT_ID = 2;
+    /*package*/ static final int VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID = 3;
 
     private static final VolumeShaper.Configuration DUCK_VSHAPE =
             new VolumeShaper.Configuration.Builder()
@@ -73,6 +82,18 @@
                     .createIfNeeded()
                     .build();
 
+    private static final long UNMUTE_DURATION_MS = 100;
+    private static final VolumeShaper.Configuration MUTE_AWAIT_CONNECTION_VSHAPE =
+            new VolumeShaper.Configuration.Builder()
+                    .setId(VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID)
+                    .setCurve(new float[] { 0.f, 1.f } /* times */,
+                            new float[] { 1.f, 0.f } /* volumes */)
+                    .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
+                    // even though we specify a duration, it's only used for the unmute,
+                    // for muting this volume shaper is run with PLAY_SKIP_RAMP
+                    .setDuration(UNMUTE_DURATION_MS)
+                    .build();
+
     // TODO support VolumeShaper on those players
     private static final int[] UNDUCKABLE_PLAYER_TYPES = {
             AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO,
@@ -90,6 +111,7 @@
     private boolean mHasPublicClients = false;
 
     private final Object mPlayerLock = new Object();
+    @GuardedBy("mPlayerLock")
     private final HashMap<Integer, AudioPlaybackConfiguration> mPlayers =
             new HashMap<Integer, AudioPlaybackConfiguration>();
 
@@ -97,12 +119,16 @@
     private int mSavedAlarmVolume = -1;
     private final int mMaxAlarmVolume;
     private int mPrivilegedAlarmActiveCount = 0;
+    private final Consumer<AudioDeviceAttributes> mMuteAwaitConnectionTimeoutCb;
 
-    PlaybackActivityMonitor(Context context, int maxAlarmVolume) {
+    PlaybackActivityMonitor(Context context, int maxAlarmVolume,
+            Consumer<AudioDeviceAttributes> muteTimeoutCallback) {
         mContext = context;
         mMaxAlarmVolume = maxAlarmVolume;
         PlayMonitorClient.sListenerDeathMonitor = this;
         AudioPlaybackConfiguration.sPlayerDeathMonitor = this;
+        mMuteAwaitConnectionTimeoutCb = muteTimeoutCallback;
+        initEventHandler();
     }
 
     //=================================================================
@@ -170,6 +196,7 @@
         sEventLogger.log(new NewPlayerEvent(apc));
         synchronized(mPlayerLock) {
             mPlayers.put(newPiid, apc);
+            maybeMutePlayerAwaitingConnection(apc);
         }
         return newPiid;
     }
@@ -323,6 +350,7 @@
                 mPlayers.remove(new Integer(piid));
                 mDuckingManager.removeReleased(apc);
                 mFadingManager.removeReleased(apc);
+                mMutedPlayersAwaitingConnection.remove(Integer.valueOf(piid));
                 checkVolumeForPrivilegedAlarm(apc, AudioPlaybackConfiguration.PLAYER_STATE_RELEASED);
                 change = apc.handleStateEvent(AudioPlaybackConfiguration.PLAYER_STATE_RELEASED,
                         AudioPlaybackConfiguration.PLAYER_DEVICEID_INVALID);
@@ -451,7 +479,7 @@
             pw.println("\n  faded out players piids:");
             mFadingManager.dump(pw);
             // players muted due to the device ringing or being in a call
-            pw.print("\n  muted player piids:");
+            pw.print("\n  muted player piids due to call/ring:");
             for (int piid : mMutedPlayers) {
                 pw.print(" " + piid);
             }
@@ -462,6 +490,12 @@
                 pw.print(" " + uid);
             }
             pw.println("\n");
+            // muted players:
+            pw.print("\n  muted players (piids) awaiting device connection: BL3 ####");
+            for (int piid : mMutedPlayersAwaitingConnection) {
+                pw.print(" " + piid);
+            }
+            pw.println("\n");
             // log
             sEventLogger.dump(pw);
         }
@@ -1100,6 +1134,155 @@
         }
     }
 
+    private static final class MuteAwaitConnectionEvent extends AudioEventLogger.Event {
+        private final @NonNull int[] mUsagesToMute;
+
+        MuteAwaitConnectionEvent(@NonNull int[] usagesToMute) {
+            mUsagesToMute = usagesToMute;
+        }
+
+        @Override
+        public String eventToString() {
+            return "muteAwaitConnection muting usages " + Arrays.toString(mUsagesToMute);
+        }
+    }
+
     static final AudioEventLogger sEventLogger = new AudioEventLogger(100,
             "playback activity as reported through PlayerBase");
+
+    //==========================================================================================
+    // Mute conditional on device connection
+    //==========================================================================================
+    void muteAwaitConnection(@NonNull int[] usagesToMute,
+            @NonNull AudioDeviceAttributes dev, long timeOutMs) {
+        synchronized (mPlayerLock) {
+            mutePlayersExpectingDevice(usagesToMute);
+            // schedule timeout (remove previously scheduled first)
+            mEventHandler.removeMessages(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION);
+            mEventHandler.sendMessageDelayed(
+                    mEventHandler.obtainMessage(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION, dev),
+                    timeOutMs);
+        }
+    }
+
+    void cancelMuteAwaitConnection() {
+        synchronized (mPlayerLock) {
+            // cancel scheduled timeout, ignore device, only one expected device at a time
+            mEventHandler.removeMessages(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION);
+            // unmute immediately
+            unmutePlayersExpectingDevice();
+        }
+    }
+
+    /**
+     * List of the piids of the players that are muted until a specific audio device connects
+     */
+    @GuardedBy("mPlayerLock")
+    private final ArrayList<Integer> mMutedPlayersAwaitingConnection = new ArrayList<Integer>();
+
+    /**
+     * List of AudioAttributes usages to mute until a specific audio device connects
+     */
+    @GuardedBy("mPlayerLock")
+    private @Nullable int[] mMutedUsagesAwaitingConnection = null;
+
+    @GuardedBy("mPlayerLock")
+    private void mutePlayersExpectingDevice(@NonNull int[] usagesToMute) {
+        sEventLogger.log(new MuteAwaitConnectionEvent(usagesToMute));
+        mMutedUsagesAwaitingConnection = usagesToMute;
+        final Set<Integer> piidSet = mPlayers.keySet();
+        final Iterator<Integer> piidIterator = piidSet.iterator();
+        // find which players to mute
+        while (piidIterator.hasNext()) {
+            final Integer piid = piidIterator.next();
+            final AudioPlaybackConfiguration apc = mPlayers.get(piid);
+            if (apc == null) {
+                continue;
+            }
+            maybeMutePlayerAwaitingConnection(apc);
+        }
+    }
+
+    @GuardedBy("mPlayerLock")
+    private void maybeMutePlayerAwaitingConnection(@NonNull AudioPlaybackConfiguration apc) {
+        if (mMutedUsagesAwaitingConnection == null) {
+            return;
+        }
+        for (int usage : mMutedUsagesAwaitingConnection) {
+            if (usage == apc.getAudioAttributes().getUsage()) {
+                try {
+                    sEventLogger.log((new AudioEventLogger.StringEvent(
+                            "awaiting connection: muting piid:"
+                                    + apc.getPlayerInterfaceId()
+                                    + " uid:" + apc.getClientUid())).printLog(TAG));
+                    apc.getPlayerProxy().applyVolumeShaper(
+                            MUTE_AWAIT_CONNECTION_VSHAPE,
+                            PLAY_CREATE_IF_NEEDED);
+                    mMutedPlayersAwaitingConnection.add(apc.getPlayerInterfaceId());
+                } catch (Exception e) {
+                    Log.e(TAG, "awaiting connection: error muting player "
+                            + apc.getPlayerInterfaceId(), e);
+                }
+            }
+        }
+    }
+
+    @GuardedBy("mPlayerLock")
+    private void unmutePlayersExpectingDevice() {
+        if (mMutedPlayersAwaitingConnection.isEmpty()) {
+            return;
+        }
+        for (int piid : mMutedPlayersAwaitingConnection) {
+            final AudioPlaybackConfiguration apc = mPlayers.get(piid);
+            if (apc == null) {
+                continue;
+            }
+            try {
+                sEventLogger.log(new AudioEventLogger.StringEvent(
+                        "unmuting piid:" + piid).printLog(TAG));
+                apc.getPlayerProxy().applyVolumeShaper(MUTE_AWAIT_CONNECTION_VSHAPE,
+                        VolumeShaper.Operation.REVERSE);
+            } catch (Exception e) {
+                Log.e(TAG, "Error unmuting player " + piid + " uid:"
+                        + apc.getClientUid(), e);
+            }
+        }
+        mMutedPlayersAwaitingConnection.clear();
+        mMutedUsagesAwaitingConnection = null;
+    }
+
+    //=================================================================
+    // Message handling
+    private Handler mEventHandler;
+    private HandlerThread mEventThread;
+
+    /**
+     * timeout for a mute awaiting a device connection
+     * args:
+     *     msg.obj: the audio device being expected
+     *         type: AudioDeviceAttributes
+     */
+    private static final int MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION = 1;
+
+    private void initEventHandler() {
+        mEventThread = new HandlerThread(TAG);
+        mEventThread.start();
+        mEventHandler = new Handler(mEventThread.getLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION:
+                        Log.i(TAG, "Timeout for muting waiting for "
+                                + (AudioDeviceAttributes) msg.obj + ", unmuting");
+                        synchronized (mPlayerLock) {
+                            unmutePlayersExpectingDevice();
+                        }
+                        mMuteAwaitConnectionTimeoutCb.accept((AudioDeviceAttributes) msg.obj);
+                        break;
+                    default:
+                        break;
+                }
+            }
+        };
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index f42870b..758cf7a 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -1036,7 +1036,8 @@
         promptInfo.setAuthenticators(authenticators);
 
         return PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors,
-                userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */);
+                userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */,
+                getContext());
     }
 
     /**
@@ -1375,7 +1376,8 @@
             try {
                 final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager,
                         mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo,
-                        opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists());
+                        opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(),
+                        getContext());
 
                 final Pair<Integer, Integer> preAuthStatus = preAuthInfo.getPreAuthenticateStatus();
 
@@ -1383,8 +1385,11 @@
                         + "), status(" + preAuthStatus.second + "), preAuthInfo: " + preAuthInfo
                         + " requestId: " + requestId + " promptInfo.isIgnoreEnrollmentState: "
                         + promptInfo.isIgnoreEnrollmentState());
-
-                if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS) {
+                // BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED is added so that BiometricPrompt can
+                // be shown for this case.
+                if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS
+                        || preAuthStatus.second
+                        == BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED) {
                     // If BIOMETRIC_WEAK or BIOMETRIC_STRONG are allowed, but not enrolled, but
                     // CREDENTIAL is requested and available, set the bundle to only request
                     // CREDENTIAL.
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index a5a3542..05c3f68 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -26,6 +26,8 @@
 import android.annotation.NonNull;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.ITrustManager;
+import android.content.Context;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.PromptInfo;
@@ -59,6 +61,7 @@
     static final int CREDENTIAL_NOT_ENROLLED = 9;
     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,
@@ -69,7 +72,8 @@
             BIOMETRIC_NOT_ENABLED_FOR_APPS,
             CREDENTIAL_NOT_ENROLLED,
             BIOMETRIC_LOCKOUT_TIMED,
-            BIOMETRIC_LOCKOUT_PERMANENT})
+            BIOMETRIC_LOCKOUT_PERMANENT,
+            BIOMETRIC_SENSOR_PRIVACY_ENABLED})
     @Retention(RetentionPolicy.SOURCE)
     @interface AuthenticatorStatus {}
 
@@ -84,13 +88,15 @@
     final boolean credentialAvailable;
     final boolean confirmationRequested;
     final boolean ignoreEnrollmentState;
+    final int userId;
+    final Context context;
 
     static PreAuthInfo create(ITrustManager trustManager,
             DevicePolicyManager devicePolicyManager,
             BiometricService.SettingObserver settingObserver,
             List<BiometricSensor> sensors,
             int userId, PromptInfo promptInfo, String opPackageName,
-            boolean checkDevicePolicyManager)
+            boolean checkDevicePolicyManager, Context context)
             throws RemoteException {
 
         final boolean confirmationRequested = promptInfo.isConfirmationRequested();
@@ -116,14 +122,22 @@
                         devicePolicyManager, settingObserver, sensor, userId, opPackageName,
                         checkDevicePolicyManager, requestedStrength,
                         promptInfo.getAllowedSensorIds(),
-                        promptInfo.isIgnoreEnrollmentState());
+                        promptInfo.isIgnoreEnrollmentState(),
+                        context);
 
                 Slog.d(TAG, "Package: " + opPackageName
                         + " Sensor ID: " + sensor.id
                         + " Modality: " + sensor.modality
                         + " Status: " + status);
 
-                if (status == AUTHENTICATOR_OK) {
+                // A sensor with privacy enabled will still be eligible to
+                // authenticate with biometric prompt. This is so the framework can display
+                // a sensor privacy error message to users after briefly showing the
+                // Biometric Prompt.
+                //
+                // Note: if only a certain sensor is required and the privacy is enabled,
+                // canAuthenticate() will return false.
+                if (status == AUTHENTICATOR_OK || status == BIOMETRIC_SENSOR_PRIVACY_ENABLED) {
                     eligibleSensors.add(sensor);
                 } else {
                     ineligibleSensors.add(new Pair<>(sensor, status));
@@ -133,7 +147,7 @@
 
         return new PreAuthInfo(biometricRequested, requestedStrength, credentialRequested,
                 eligibleSensors, ineligibleSensors, credentialAvailable, confirmationRequested,
-                promptInfo.isIgnoreEnrollmentState());
+                promptInfo.isIgnoreEnrollmentState(), userId, context);
     }
 
     /**
@@ -149,7 +163,7 @@
             BiometricSensor sensor, int userId, String opPackageName,
             boolean checkDevicePolicyManager, int requestedStrength,
             @NonNull List<Integer> requestedSensorIds,
-            boolean ignoreEnrollmentState) {
+            boolean ignoreEnrollmentState, Context context) {
 
         if (!requestedSensorIds.isEmpty() && !requestedSensorIds.contains(sensor.id)) {
             return BIOMETRIC_NO_HARDWARE;
@@ -175,6 +189,16 @@
                     && !ignoreEnrollmentState) {
                 return BIOMETRIC_NOT_ENROLLED;
             }
+            final SensorPrivacyManager sensorPrivacyManager = context
+                    .getSystemService(SensorPrivacyManager.class);
+
+            if (sensorPrivacyManager != null && sensor.modality == TYPE_FACE) {
+                if (sensorPrivacyManager
+                        .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId)) {
+                    return BIOMETRIC_SENSOR_PRIVACY_ENABLED;
+                }
+            }
+
 
             final @LockoutTracker.LockoutMode int lockoutMode =
                     sensor.impl.getLockoutModeForUser(userId);
@@ -243,7 +267,8 @@
     private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested,
             boolean credentialRequested, List<BiometricSensor> eligibleSensors,
             List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable,
-            boolean confirmationRequested, boolean ignoreEnrollmentState) {
+            boolean confirmationRequested, boolean ignoreEnrollmentState, int userId,
+            Context context) {
         mBiometricRequested = biometricRequested;
         mBiometricStrengthRequested = biometricStrengthRequested;
         this.credentialRequested = credentialRequested;
@@ -253,6 +278,8 @@
         this.credentialAvailable = credentialAvailable;
         this.confirmationRequested = confirmationRequested;
         this.ignoreEnrollmentState = ignoreEnrollmentState;
+        this.userId = userId;
+        this.context = context;
     }
 
     private Pair<BiometricSensor, Integer> calculateErrorByPriority() {
@@ -280,15 +307,35 @@
     private Pair<Integer, Integer> getInternalStatus() {
         @AuthenticatorStatus final int status;
         @BiometricAuthenticator.Modality int modality = TYPE_NONE;
+
+        final SensorPrivacyManager sensorPrivacyManager = context
+                .getSystemService(SensorPrivacyManager.class);
+
+        boolean cameraPrivacyEnabled = false;
+        if (sensorPrivacyManager != null) {
+            cameraPrivacyEnabled = sensorPrivacyManager
+                    .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId);
+        }
+
         if (mBiometricRequested && credentialRequested) {
             if (credentialAvailable || !eligibleSensors.isEmpty()) {
-                status = AUTHENTICATOR_OK;
-                if (credentialAvailable) {
-                    modality |= TYPE_CREDENTIAL;
-                }
                 for (BiometricSensor sensor : eligibleSensors) {
                     modality |= sensor.modality;
                 }
+
+                if (credentialAvailable) {
+                    modality |= TYPE_CREDENTIAL;
+                    status = AUTHENTICATOR_OK;
+                } else if (modality == TYPE_FACE && cameraPrivacyEnabled) {
+                    // If the only modality requested is face, credential is unavailable,
+                    // and the face sensor privacy is enabled then return
+                    // BIOMETRIC_SENSOR_PRIVACY_ENABLED.
+                    //
+                    // Note: This sensor will still be eligible for calls to authenticate.
+                    status = BIOMETRIC_SENSOR_PRIVACY_ENABLED;
+                } else {
+                    status = AUTHENTICATOR_OK;
+                }
             } else {
                 // Pick the first sensor error if it exists
                 if (!ineligibleSensors.isEmpty()) {
@@ -302,10 +349,18 @@
             }
         } else if (mBiometricRequested) {
             if (!eligibleSensors.isEmpty()) {
-                 status = AUTHENTICATOR_OK;
-                 for (BiometricSensor sensor : eligibleSensors) {
-                     modality |= sensor.modality;
-                 }
+                for (BiometricSensor sensor : eligibleSensors) {
+                    modality |= sensor.modality;
+                }
+                if (modality == TYPE_FACE && cameraPrivacyEnabled) {
+                    // If the only modality requested is face and the privacy is enabled
+                    // then return BIOMETRIC_SENSOR_PRIVACY_ENABLED.
+                    //
+                    // Note: This sensor will still be eligible for calls to authenticate.
+                    status = BIOMETRIC_SENSOR_PRIVACY_ENABLED;
+                } else {
+                    status = AUTHENTICATOR_OK;
+                }
             } else {
                 // Pick the first sensor error if it exists
                 if (!ineligibleSensors.isEmpty()) {
@@ -326,9 +381,9 @@
             Slog.e(TAG, "No authenticators requested");
             status = BIOMETRIC_NO_HARDWARE;
         }
-
         Slog.d(TAG, "getCanAuthenticateInternal Modality: " + modality
                 + " AuthenticatorStatus: " + status);
+
         return new Pair<>(modality, status);
     }
 
@@ -362,6 +417,7 @@
             case CREDENTIAL_NOT_ENROLLED:
             case BIOMETRIC_LOCKOUT_TIMED:
             case BIOMETRIC_LOCKOUT_PERMANENT:
+            case BIOMETRIC_SENSOR_PRIVACY_ENABLED:
                 break;
 
             case BIOMETRIC_DISABLED_BY_DEVICE_POLICY:
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 4f7c6b0..0e2582c 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -33,6 +33,7 @@
 import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_NOT_ENABLED_FOR_APPS;
 import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_NOT_ENROLLED;
 import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_NO_HARDWARE;
+import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_SENSOR_PRIVACY_ENABLED;
 import static com.android.server.biometrics.PreAuthInfo.CREDENTIAL_NOT_ENROLLED;
 
 import android.annotation.NonNull;
@@ -278,6 +279,9 @@
             case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
                 biometricManagerCode = BiometricManager.BIOMETRIC_SUCCESS;
                 break;
+            case BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED:
+                biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
+                break;
             default:
                 Slog.e(BiometricService.TAG, "Unhandled result code: " + biometricConstantsCode);
                 biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
@@ -337,7 +341,8 @@
 
             case BIOMETRIC_LOCKOUT_PERMANENT:
                 return BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
-
+            case BIOMETRIC_SENSOR_PRIVACY_ENABLED:
+                return BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED;
             case BIOMETRIC_DISABLED_BY_DEVICE_POLICY:
             case BIOMETRIC_HARDWARE_NOT_DETECTED:
             case BIOMETRIC_NOT_ENABLED_FOR_APPS:
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 97d791b..4131ae1 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
@@ -21,6 +21,7 @@
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.res.Resources;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricFaceConstants;
@@ -56,6 +57,7 @@
     @NonNull private final LockoutCache mLockoutCache;
     @Nullable private final NotificationManager mNotificationManager;
     @Nullable private ICancellationSignal mCancellationSignal;
+    @Nullable private SensorPrivacyManager mSensorPrivacyManager;
 
     private final int[] mBiometricPromptIgnoreList;
     private final int[] mBiometricPromptIgnoreListVendor;
@@ -81,6 +83,7 @@
         mUsageStats = usageStats;
         mLockoutCache = lockoutCache;
         mNotificationManager = context.getSystemService(NotificationManager.class);
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
 
         final Resources resources = getContext().getResources();
         mBiometricPromptIgnoreList = resources.getIntArray(
@@ -108,7 +111,16 @@
     @Override
     protected void startHalOperation() {
         try {
-            mCancellationSignal = getFreshDaemon().authenticate(mOperationId);
+            if (mSensorPrivacyManager != null
+                    && mSensorPrivacyManager
+                    .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA,
+                    getTargetUserId())) {
+                onError(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE,
+                        0 /* vendorCode */);
+                mCallback.onClientFinished(this, false /* success */);
+            } else {
+                mCancellationSignal = getFreshDaemon().authenticate(mOperationId);
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when requesting auth", e);
             onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java
index 2ef0911..2158dfe 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.SensorPrivacyManager;
+import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.common.ICancellationSignal;
 import android.hardware.biometrics.face.ISession;
@@ -41,6 +43,7 @@
 
     private final boolean mIsStrongBiometric;
     @Nullable private ICancellationSignal mCancellationSignal;
+    @Nullable private SensorPrivacyManager mSensorPrivacyManager;
 
     public FaceDetectClient(@NonNull Context context, @NonNull LazyDaemon<ISession> lazyDaemon,
             @NonNull IBinder token, long requestId,
@@ -51,6 +54,7 @@
                 BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient);
         setRequestId(requestId);
         mIsStrongBiometric = isStrongBiometric;
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
     }
 
     @Override
@@ -73,6 +77,14 @@
 
     @Override
     protected void startHalOperation() {
+        if (mSensorPrivacyManager != null
+                && mSensorPrivacyManager
+                .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, getTargetUserId())) {
+            onError(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */);
+            mCallback.onClientFinished(this, false /* success */);
+            return;
+        }
+
         try {
             mCancellationSignal = getFreshDaemon().detectInteraction();
         } catch (RemoteException e) {
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 40f2801..7548d28 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
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.content.res.Resources;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricFaceConstants;
@@ -55,6 +56,7 @@
     private final int[] mKeyguardIgnoreListVendor;
 
     private int mLastAcquire;
+    private SensorPrivacyManager mSensorPrivacyManager;
 
     FaceAuthenticationClient(@NonNull Context context,
             @NonNull LazyDaemon<IBiometricsFace> lazyDaemon,
@@ -71,6 +73,7 @@
                 isKeyguardBypassEnabled);
         setRequestId(requestId);
         mUsageStats = usageStats;
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
 
         final Resources resources = getContext().getResources();
         mBiometricPromptIgnoreList = resources.getIntArray(
@@ -97,6 +100,15 @@
 
     @Override
     protected void startHalOperation() {
+
+        if (mSensorPrivacyManager != null
+                && mSensorPrivacyManager
+                .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, getTargetUserId())) {
+            onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */);
+            mCallback.onClientFinished(this, false /* success */);
+            return;
+        }
+
         try {
             getFreshDaemon().authenticate(mOperationId);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/communal/CommunalManagerService.java b/services/core/java/com/android/server/communal/CommunalManagerService.java
index 1196442..8d9b13e 100644
--- a/services/core/java/com/android/server/communal/CommunalManagerService.java
+++ b/services/core/java/com/android/server/communal/CommunalManagerService.java
@@ -84,6 +84,10 @@
                 @Override
                 public Intent intercept(ActivityInterceptorInfo info) {
                     if (!shouldIntercept(info.aInfo)) {
+                        if (DEBUG) {
+                            Slog.d(TAG, "Activity allowed, not intercepting: "
+                                    + info.aInfo.getComponentName());
+                        }
                         return null;
                     }
 
@@ -188,8 +192,17 @@
             return true;
         }
 
-        return isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, appInfo)
-                && getUserEnabledApps().contains(appInfo.packageName);
+        if (!isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, appInfo)) {
+            if (DEBUG) Slog.d(TAG, "App is not allowlisted: " + appInfo.packageName);
+            return false;
+        }
+
+        if (!getUserEnabledApps().contains(appInfo.packageName)) {
+            if (DEBUG) Slog.d(TAG, "App does not have user consent: " + appInfo.packageName);
+            return false;
+        }
+
+        return true;
     }
 
     private boolean isActiveDream(ApplicationInfo appInfo) {
@@ -219,6 +232,10 @@
         final boolean showWhenLocked =
                 (activityInfo.flags & ActivityInfo.FLAG_SHOW_WHEN_LOCKED) != 0;
         if (!showWhenLocked) {
+            if (DEBUG) {
+                Slog.d(TAG, "Activity does not contain showWhenLocked attribute: "
+                        + activityInfo.getComponentName());
+            }
             return true;
         }
 
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 26a5bbb..356d6c9 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -286,6 +286,7 @@
 
         synchronized (mSessions) {
             readSessionsLocked();
+            expireSessionsLocked();
 
             reconcileStagesLocked(StorageManager.UUID_PRIVATE_INTERNAL);
 
@@ -462,34 +463,7 @@
                             Slog.e(TAG, "Could not read session", e);
                             continue;
                         }
-
-                        final long age = System.currentTimeMillis() - session.createdMillis;
-                        final long timeSinceUpdate =
-                                System.currentTimeMillis() - session.getUpdatedMillis();
-                        final boolean valid;
-                        if (session.isStaged()) {
-                            if (timeSinceUpdate >= MAX_TIME_SINCE_UPDATE_MILLIS
-                                    && session.isStagedAndInTerminalState()) {
-                                valid = false;
-                            } else {
-                                valid = true;
-                            }
-                        } else if (age >= MAX_AGE_MILLIS) {
-                            Slog.w(TAG, "Abandoning old session created at "
-                                        + session.createdMillis);
-                            valid = false;
-                        } else {
-                            valid = true;
-                        }
-
-                        if (valid) {
-                            mSessions.put(session.sessionId, session);
-                        } else {
-                            // Since this is early during boot we don't send
-                            // any observer events about the session, but we
-                            // keep details around for dumpsys.
-                            addHistoricalSessionLocked(session);
-                        }
+                        mSessions.put(session.sessionId, session);
                         mAllocatedSessions.put(session.sessionId, true);
                     }
                 }
@@ -509,6 +483,44 @@
     }
 
     @GuardedBy("mSessions")
+    private void expireSessionsLocked() {
+        SparseArray<PackageInstallerSession> tmp = mSessions.clone();
+        final int n = tmp.size();
+        for (int i = 0; i < n; ++i) {
+            PackageInstallerSession session = tmp.valueAt(i);
+            if (session.hasParentSessionId()) {
+                // Child sessions will be expired when handling parent sessions
+                continue;
+            }
+            final long age = System.currentTimeMillis() - session.createdMillis;
+            final long timeSinceUpdate = System.currentTimeMillis() - session.getUpdatedMillis();
+            final boolean valid;
+            if (session.isStaged()) {
+                valid = !session.isStagedAndInTerminalState()
+                        || timeSinceUpdate < MAX_TIME_SINCE_UPDATE_MILLIS;
+            } else if (age >= MAX_AGE_MILLIS) {
+                Slog.w(TAG, "Abandoning old session created at "
+                        + session.createdMillis);
+                valid = false;
+            } else {
+                valid = true;
+            }
+            if (!valid) {
+                // Remove expired sessions as well as child sessions if any
+                mSessions.remove(session.sessionId);
+                // Since this is early during boot we don't send
+                // any observer events about the session, but we
+                // keep details around for dumpsys.
+                addHistoricalSessionLocked(session);
+                for (PackageInstallerSession child : session.getChildSessions()) {
+                    mSessions.remove(child.sessionId);
+                    addHistoricalSessionLocked(child);
+                }
+            }
+        }
+    }
+
+    @GuardedBy("mSessions")
     private void addHistoricalSessionLocked(PackageInstallerSession session) {
         CharArrayWriter writer = new CharArrayWriter();
         IndentingPrintWriter pw = new IndentingPrintWriter(writer, "    ");
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 232ea09..4eae939 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -64,6 +64,7 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -139,6 +140,8 @@
     private int mCurrentUserId;
     private boolean mTracingEnabled;
 
+    private final TileRequestTracker mTileRequestTracker;
+
     private final SparseArray<UiState> mDisplayUiState = new SparseArray<>();
     @GuardedBy("mLock")
     private IUdfpsHbmListener mUdfpsHbmListener;
@@ -245,6 +248,8 @@
         mActivityTaskManager = LocalServices.getService(ActivityTaskManagerInternal.class);
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+
+        mTileRequestTracker = new TileRequestTracker(mContext);
     }
 
     @Override
@@ -1765,11 +1770,26 @@
             mCurrentRequestAddTilePackages.put(packageName, currentTime);
         }
 
+        if (mTileRequestTracker.shouldBeDenied(userId, componentName)) {
+            if (clearTileAddRequest(packageName)) {
+                try {
+                    callback.onTileRequest(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "requestAddTile - callback", e);
+                }
+            }
+            return;
+        }
+
         IAddTileResultCallback proxyCallback = new IAddTileResultCallback.Stub() {
             @Override
             public void onTileRequest(int i) {
                 if (i == StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED) {
                     i = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED;
+                } else if (i == StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED) {
+                    mTileRequestTracker.addDenial(userId, componentName);
+                } else if (i == StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED) {
+                    mTileRequestTracker.resetRequests(userId, componentName);
                 }
                 if (clearTileAddRequest(packageName)) {
                     try {
@@ -1961,6 +1981,8 @@
                 pw.println("    " + requests.get(i) + ",");
             }
             pw.println("  ]");
+            IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+            mTileRequestTracker.dump(fd, ipw.increaseIndent(), args);
         }
     }
 
diff --git a/services/core/java/com/android/server/statusbar/TileRequestTracker.java b/services/core/java/com/android/server/statusbar/TileRequestTracker.java
new file mode 100644
index 0000000..d5ace3f7
--- /dev/null
+++ b/services/core/java/com/android/server/statusbar/TileRequestTracker.java
@@ -0,0 +1,138 @@
+/*
+ * 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.statusbar;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.SparseArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+
+/**
+ * Tracks user denials of requests from {@link StatusBarManagerService#requestAddTile}.
+ *
+ * After a certain number of denials for a particular pair (user,ComponentName), requests will be
+ * auto-denied without showing a dialog to the user.
+ */
+public class TileRequestTracker {
+
+    @VisibleForTesting
+    static final int MAX_NUM_DENIALS = 3;
+
+    private final Context mContext;
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private final SparseArrayMap<ComponentName, Integer> mTrackingMap = new SparseArrayMap<>();
+    @GuardedBy("mLock")
+    private final ArraySet<ComponentName> mComponentsToRemove = new ArraySet<>();
+
+    private final BroadcastReceiver mUninstallReceiver = new BroadcastReceiver() {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+                return;
+            }
+
+            Uri data = intent.getData();
+            String packageName = data.getEncodedSchemeSpecificPart();
+
+            if (!intent.hasExtra(Intent.EXTRA_UID)) {
+                return;
+            }
+            int userId = UserHandle.getUserId(intent.getIntExtra(Intent.EXTRA_UID, -1));
+            synchronized (mLock) {
+                mComponentsToRemove.clear();
+                final int elementsForUser = mTrackingMap.numElementsForKey(userId);
+                final int userKeyIndex = mTrackingMap.indexOfKey(userId);
+                for (int compKeyIndex = 0; compKeyIndex < elementsForUser; compKeyIndex++) {
+                    ComponentName c = mTrackingMap.keyAt(userKeyIndex, compKeyIndex);
+                    if (c.getPackageName().equals(packageName)) {
+                        mComponentsToRemove.add(c);
+                    }
+                }
+                final int compsToRemoveNum = mComponentsToRemove.size();
+                for (int i = 0; i < compsToRemoveNum; i++) {
+                    ComponentName c = mComponentsToRemove.valueAt(i);
+                    mTrackingMap.delete(userId, c);
+                }
+            }
+        }
+    };
+
+    TileRequestTracker(Context context) {
+        mContext = context;
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        intentFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
+        intentFilter.addDataScheme("package");
+        mContext.registerReceiverAsUser(mUninstallReceiver, UserHandle.ALL, intentFilter, null,
+                null);
+    }
+
+    /**
+     * Return whether this combination of {@code userId} and {@link ComponentName} should be
+     * auto-denied.
+     */
+    boolean shouldBeDenied(int userId, ComponentName componentName) {
+        synchronized (mLock) {
+            return mTrackingMap.getOrDefault(userId, componentName, 0) >= MAX_NUM_DENIALS;
+        }
+    }
+
+    /**
+     * Add a new denial instance for a given {@code userId} and {@link ComponentName}.
+     */
+    void addDenial(int userId, ComponentName componentName) {
+        synchronized (mLock) {
+            int current = mTrackingMap.getOrDefault(userId, componentName, 0);
+            mTrackingMap.add(userId, componentName, current + 1);
+        }
+    }
+
+    /**
+     * Reset the number of denied request for a given {@code userId} and {@link ComponentName}.
+     */
+    void resetRequests(int userId, ComponentName componentName) {
+        synchronized (mLock) {
+            mTrackingMap.delete(userId, componentName);
+        }
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
+        pw.println("TileRequestTracker:");
+        pw.increaseIndent();
+        synchronized (mLock) {
+            mTrackingMap.forEach((user, componentName, value) -> {
+                pw.println("user=" + user + ", " + componentName.toShortString() + ": " + value);
+            });
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/services/core/java/com/android/server/timedetector/ServerFlags.java b/services/core/java/com/android/server/timedetector/ServerFlags.java
index 65907f1..417177f 100644
--- a/services/core/java/com/android/server/timedetector/ServerFlags.java
+++ b/services/core/java/com/android/server/timedetector/ServerFlags.java
@@ -35,6 +35,7 @@
 import java.time.DateTimeException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -179,8 +180,13 @@
     public static final @DeviceConfigKey String KEY_ENHANCED_METRICS_COLLECTION_ENABLED =
             "enhanced_metrics_collection_enabled";
 
+    /**
+     * The registered listeners and the keys to trigger on. The value is explicitly a HashSet to
+     * ensure O(1) lookup performance when working out whether a listener should trigger.
+     */
     @GuardedBy("mListeners")
-    private final ArrayMap<ConfigurationChangeListener, Set<String>> mListeners = new ArrayMap<>();
+    private final ArrayMap<ConfigurationChangeListener, HashSet<String>> mListeners =
+            new ArrayMap<>();
 
     private static final Object SLOCK = new Object();
 
@@ -207,18 +213,29 @@
 
     private void handlePropertiesChanged(@NonNull DeviceConfig.Properties properties) {
         synchronized (mListeners) {
-            for (Map.Entry<ConfigurationChangeListener, Set<String>> listenerEntry
+            for (Map.Entry<ConfigurationChangeListener, HashSet<String>> listenerEntry
                     : mListeners.entrySet()) {
-                if (intersects(listenerEntry.getValue(), properties.getKeyset())) {
+                // 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
+                // only changed keys are reported. Because we guarantee the type / lookup behavior
+                // of the monitoredKeys by making that a HashSet, that is used as the haystack Set,
+                // while the changed keys is treated as the needles Iterable. At the time of
+                // writing, properties.getKeyset() actually returns a HashSet, so iteration isn't
+                // super efficient and the use of HashSet for monitoredKeys may be redundant, but
+                // neither set will be enormous.
+                HashSet<String> monitoredKeys = listenerEntry.getValue();
+                Iterable<String> modifiedKeys = properties.getKeyset();
+                if (containsAny(monitoredKeys, modifiedKeys)) {
                     listenerEntry.getKey().onChange();
                 }
             }
         }
     }
 
-    private static boolean intersects(@NonNull Set<String> one, @NonNull Set<String> two) {
-        for (String toFind : one) {
-            if (two.contains(toFind)) {
+    private static boolean containsAny(
+            @NonNull Set<String> haystack, @NonNull Iterable<String> needles) {
+        for (String needle : needles) {
+            if (haystack.contains(needle)) {
                 return true;
             }
         }
@@ -237,8 +254,11 @@
         Objects.requireNonNull(listener);
         Objects.requireNonNull(keys);
 
+        // Make a defensive copy and use a well-defined Set implementation to provide predictable
+        // performance on the lookup.
+        HashSet<String> keysCopy = new HashSet<>(keys);
         synchronized (mListeners) {
-            mListeners.put(listener, keys);
+            mListeners.put(listener, keysCopy);
         }
     }
 
diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
index 7f7d01c..5f14000 100644
--- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
+++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
@@ -25,7 +25,6 @@
 import android.content.Context;
 import android.os.Build;
 import android.os.SystemProperties;
-import android.util.ArraySet;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -35,7 +34,6 @@
 
 import java.time.Instant;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -65,11 +63,10 @@
             Long.max(android.os.Environment.getRootDirectory().lastModified(), Build.TIME));
 
     /** Device config keys that affect the {@link TimeDetectorService}. */
-    private static final Set<String> SERVER_FLAGS_KEYS_TO_WATCH = Collections.unmodifiableSet(
-            new ArraySet<>(new String[] {
-                    KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE,
-                    KEY_TIME_DETECTOR_ORIGIN_PRIORITIES_OVERRIDE,
-            }));
+    private static final Set<String> SERVER_FLAGS_KEYS_TO_WATCH = Set.of(
+            KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE,
+            KEY_TIME_DETECTOR_ORIGIN_PRIORITIES_OVERRIDE
+    );
 
     private static final Object SLOCK = new Object();
 
diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
index b452d90..ae52912 100644
--- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
@@ -36,7 +36,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.util.ArraySet;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -45,7 +44,6 @@
 
 import java.time.Duration;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -59,33 +57,31 @@
     /**
      * Device config keys that can affect the content of {@link ConfigurationInternal}.
      */
-    private static final Set<String> CONFIGURATION_INTERNAL_SERVER_FLAGS_KEYS_TO_WATCH =
-            Collections.unmodifiableSet(new ArraySet<>(new String[] {
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_FEATURE_SUPPORTED,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_RUN_IN_BACKGROUND_ENABLED,
-                    ServerFlags.KEY_ENHANCED_METRICS_COLLECTION_ENABLED,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE,
-                    ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED,
-            }));
+    private static final Set<String> CONFIGURATION_INTERNAL_SERVER_FLAGS_KEYS_TO_WATCH = Set.of(
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_FEATURE_SUPPORTED,
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_RUN_IN_BACKGROUND_ENABLED,
+            ServerFlags.KEY_ENHANCED_METRICS_COLLECTION_ENABLED,
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT,
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE,
+            ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED
+    );
 
     /**
      * Device config keys that can affect {@link
      * com.android.server.timezonedetector.location.LocationTimeZoneManagerService} behavior.
      */
-    private static final Set<String> LOCATION_TIME_ZONE_MANAGER_SERVER_FLAGS_KEYS_TO_WATCH =
-            Collections.unmodifiableSet(new ArraySet<>(new String[] {
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_FEATURE_SUPPORTED,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_RUN_IN_BACKGROUND_ENABLED,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE,
-                    ServerFlags.KEY_PRIMARY_LTZP_MODE_OVERRIDE,
-                    ServerFlags.KEY_SECONDARY_LTZP_MODE_OVERRIDE,
-                    ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS,
-                    ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
-                    ServerFlags.KEY_LTZP_EVENT_FILTERING_AGE_THRESHOLD_MILLIS,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS
-            }));
+    private static final Set<String> LOCATION_TIME_ZONE_MANAGER_SERVER_FLAGS_KEYS_TO_WATCH = Set.of(
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_FEATURE_SUPPORTED,
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_RUN_IN_BACKGROUND_ENABLED,
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT,
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE,
+            ServerFlags.KEY_PRIMARY_LTZP_MODE_OVERRIDE,
+            ServerFlags.KEY_SECONDARY_LTZP_MODE_OVERRIDE,
+            ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS,
+            ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
+            ServerFlags.KEY_LTZP_EVENT_FILTERING_AGE_THRESHOLD_MILLIS,
+            ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS
+    );
 
     private static final Duration DEFAULT_LTZP_INITIALIZATION_TIMEOUT = Duration.ofMinutes(5);
     private static final Duration DEFAULT_LTZP_INITIALIZATION_TIMEOUT_FUZZ = Duration.ofMinutes(1);
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 0436460..6628802 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
@@ -37,6 +37,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
@@ -52,6 +53,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * This class provides a system service that manages the TV tuner resources.
@@ -64,6 +68,8 @@
 
     public static final int INVALID_CLIENT_ID = -1;
     private static final int MAX_CLIENT_PRIORITY = 1000;
+    private static final long INVALID_THREAD_ID = -1;
+    private static final long TRMS_LOCK_TIMEOUT = 500;
 
     // Map of the registered client profiles
     private Map<Integer, ClientProfile> mClientProfiles = new HashMap<>();
@@ -94,6 +100,12 @@
     // Used to synchronize the access to the service.
     private final Object mLock = new Object();
 
+    private final ReentrantLock mLockForTRMSLock = new ReentrantLock();
+    private final Condition mTunerApiLockReleasedCV = mLockForTRMSLock.newCondition();
+    private int mTunerApiLockHolder = INVALID_CLIENT_ID;
+    private long mTunerApiLockHolderThreadId = INVALID_THREAD_ID;
+    private int mTunerApiLockNestedCount = 0;
+
     public TunerResourceManagerService(@Nullable Context context) {
         super(context);
     }
@@ -511,6 +523,20 @@
         }
 
         @Override
+        public boolean acquireLock(int clientId, long clientThreadId) {
+            enforceTrmAccessPermission("acquireLock");
+            // this must not be locked with mLock
+            return acquireLockInternal(clientId, clientThreadId, TRMS_LOCK_TIMEOUT);
+        }
+
+        @Override
+        public boolean releaseLock(int clientId) {
+            enforceTrmAccessPermission("releaseLock");
+            // this must not be locked with mLock
+            return releaseLockInternal(clientId, TRMS_LOCK_TIMEOUT, false, false);
+        }
+
+        @Override
         protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
             final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
 
@@ -1194,6 +1220,187 @@
         return true;
     }
 
+    // Return value is guaranteed to be positive
+    private long getElapsedTime(long begin) {
+        long now = SystemClock.uptimeMillis();
+        long elapsed;
+        if (now >= begin) {
+            elapsed = now - begin;
+        } else {
+            elapsed = Long.MAX_VALUE - begin + now;
+            if (elapsed < 0) {
+                elapsed = Long.MAX_VALUE;
+            }
+        }
+        return elapsed;
+    }
+
+    private boolean lockForTunerApiLock(int clientId, long timeoutMS, String callerFunction) {
+        try {
+            if (mLockForTRMSLock.tryLock(timeoutMS, TimeUnit.MILLISECONDS)) {
+                return true;
+            } else {
+                Slog.e(TAG, "FAILED to lock mLockForTRMSLock in " + callerFunction
+                        + ", clientId:" + clientId + ", timeoutMS:" + timeoutMS
+                        + ", mTunerApiLockHolder:" + mTunerApiLockHolder);
+                return false;
+            }
+        } catch (InterruptedException ie) {
+            Slog.e(TAG, "exception thrown in " + callerFunction + ":" + ie);
+            if (mLockForTRMSLock.isHeldByCurrentThread()) {
+                mLockForTRMSLock.unlock();
+            }
+            return false;
+        }
+    }
+
+    private boolean acquireLockInternal(int clientId, long clientThreadId, long timeoutMS) {
+        long begin = SystemClock.uptimeMillis();
+
+        // Grab lock
+        if (!lockForTunerApiLock(clientId, timeoutMS, "acquireLockInternal()")) {
+            return false;
+        }
+
+        try {
+            boolean available = mTunerApiLockHolder == INVALID_CLIENT_ID;
+            boolean nestedSelf = (clientId == mTunerApiLockHolder)
+                    && (clientThreadId == mTunerApiLockHolderThreadId);
+            boolean recovery = false;
+
+            // Allow same thread to grab the lock multiple times
+            while (!available && !nestedSelf) {
+                // calculate how much time is left before timeout
+                long leftOverMS = timeoutMS - getElapsedTime(begin);
+                if (leftOverMS <= 0) {
+                    Slog.e(TAG, "FAILED:acquireLockInternal(" + clientId + ", " + clientThreadId
+                            + ", " + timeoutMS + ") - timed out, but will grant the lock to "
+                            + "the callee by stealing it from the current holder:"
+                            + mTunerApiLockHolder + "(" + mTunerApiLockHolderThreadId + "), "
+                            + "who likely failed to call releaseLock(), "
+                            + "to prevent this from becoming an unrecoverable error");
+                    // This should not normally happen, but there sometimes are cases where
+                    // in-flight tuner API execution gets scheduled even after binderDied(),
+                    // which can leave the in-flight execution dissappear/stopped in between
+                    // acquireLock and releaseLock
+                    recovery = true;
+                    break;
+                }
+
+                // Cond wait for left over time
+                mTunerApiLockReleasedCV.await(leftOverMS, TimeUnit.MILLISECONDS);
+
+                // Check the availability for "spurious wakeup"
+                // The case that was confirmed is that someone else can acquire this in between
+                // signal() and wakup from the above await()
+                available = mTunerApiLockHolder == INVALID_CLIENT_ID;
+
+                if (!available) {
+                    Slog.w(TAG, "acquireLockInternal(" + clientId + ", " + clientThreadId + ", "
+                            + timeoutMS + ") - woken up from cond wait, but " + mTunerApiLockHolder
+                            + "(" + mTunerApiLockHolderThreadId + ") is already holding the lock. "
+                            + "Going to wait again if timeout hasn't reached yet");
+                }
+            }
+
+            // Will always grant unless exception is thrown (or lock is already held)
+            if (available || recovery) {
+                if (DEBUG) {
+                    Slog.d(TAG, "SUCCESS:acquireLockInternal(" + clientId + ", " + clientThreadId
+                            + ", " + timeoutMS + ")");
+                }
+
+                if (mTunerApiLockNestedCount != 0) {
+                    Slog.w(TAG, "Something is wrong as nestedCount(" + mTunerApiLockNestedCount
+                            + ") is not zero. Will overriding it to 1 anyways");
+                }
+
+                // set the caller to be the holder
+                mTunerApiLockHolder = clientId;
+                mTunerApiLockHolderThreadId = clientThreadId;
+                mTunerApiLockNestedCount = 1;
+            } else if (nestedSelf) {
+                // Increment the nested count so releaseLockInternal won't signal prematuredly
+                mTunerApiLockNestedCount++;
+                if (DEBUG) {
+                    Slog.d(TAG, "acquireLockInternal(" + clientId + ", " + clientThreadId
+                            + ", " + timeoutMS + ") - nested count incremented to "
+                            + mTunerApiLockNestedCount);
+                }
+            } else {
+                Slog.e(TAG, "acquireLockInternal(" + clientId + ", " + clientThreadId
+                        + ", " + timeoutMS + ") - should not reach here");
+            }
+            // return true in "recovery" so callee knows that the deadlock is possible
+            // only when the return value is false
+            return (available || nestedSelf || recovery);
+        } catch (InterruptedException ie) {
+            Slog.e(TAG, "exception thrown in acquireLockInternal(" + clientId + ", "
+                    + clientThreadId + ", " + timeoutMS + "):" + ie);
+            return false;
+        } finally {
+            if (mLockForTRMSLock.isHeldByCurrentThread()) {
+                mLockForTRMSLock.unlock();
+            }
+        }
+    }
+
+    private boolean releaseLockInternal(int clientId, long timeoutMS,
+            boolean ignoreNestedCount, boolean suppressError) {
+        // Grab lock first
+        if (!lockForTunerApiLock(clientId, timeoutMS, "releaseLockInternal()")) {
+            return false;
+        }
+
+        try {
+            if (mTunerApiLockHolder == clientId) {
+                // Should always reach here unless called from binderDied()
+                mTunerApiLockNestedCount--;
+                if (ignoreNestedCount || mTunerApiLockNestedCount <= 0) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "SUCCESS:releaseLockInternal(" + clientId + ", " + timeoutMS
+                                + ", " + ignoreNestedCount + ", " + suppressError
+                                + ") - signaling!");
+                    }
+                    // Reset the current holder and signal
+                    mTunerApiLockHolder = INVALID_CLIENT_ID;
+                    mTunerApiLockHolderThreadId = INVALID_THREAD_ID;
+                    mTunerApiLockNestedCount = 0;
+                    mTunerApiLockReleasedCV.signal();
+                } else {
+                    if (DEBUG) {
+                        Slog.d(TAG, "releaseLockInternal(" + clientId + ", " + timeoutMS
+                                + ", " + ignoreNestedCount + ", " + suppressError
+                                + ") - NOT signaling because nested count is not zero ("
+                                + mTunerApiLockNestedCount + ")");
+                    }
+                }
+                return true;
+            } else if (mTunerApiLockHolder == INVALID_CLIENT_ID) {
+                if (!suppressError) {
+                    Slog.w(TAG, "releaseLockInternal(" + clientId + ", " + timeoutMS
+                            + ") - called while there is no current holder");
+                }
+                // No need to do anything.
+                // Shouldn't reach here unless called from binderDied()
+                return false;
+            } else {
+                if (!suppressError) {
+                    Slog.e(TAG, "releaseLockInternal(" + clientId + ", " + timeoutMS
+                            + ") - called while someone else:" + mTunerApiLockHolder
+                            + "is the current holder");
+                }
+                // Cannot reset the holder Id because it reaches here when called
+                // from binderDied()
+                return false;
+            }
+        } finally {
+            if (mLockForTRMSLock.isHeldByCurrentThread()) {
+                mLockForTRMSLock.unlock();
+            }
+        }
+    }
+
     @VisibleForTesting
     protected class ResourcesReclaimListenerRecord implements IBinder.DeathRecipient {
         private final IResourcesReclaimListener mListener;
@@ -1206,10 +1413,15 @@
 
         @Override
         public void binderDied() {
-            synchronized (mLock) {
-                if (checkClientExists(mClientId)) {
-                    removeClientProfile(mClientId);
+            try {
+                synchronized (mLock) {
+                    if (checkClientExists(mClientId)) {
+                        removeClientProfile(mClientId);
+                    }
                 }
+            } finally {
+                // reset the tuner API lock
+                releaseLockInternal(mClientId, TRMS_LOCK_TIMEOUT, true, true);
             }
         }
 
@@ -1247,6 +1459,13 @@
     protected boolean reclaimResource(int reclaimingClientId,
             @TunerResourceManager.TunerResourceType int resourceType) {
 
+        // Allowing this because:
+        // 1) serialization of resource reclaim is required in the current design
+        // 2) the outgoing transaction is handled by the system app (with
+        //    android.Manifest.permission.TUNER_RESOURCE_ACCESS), which goes through full
+        //    Google certification
+        Binder.allowBlockingForCurrentThread();
+
         // Reclaim all the resources of the share owners of the frontend that is used by the current
         // resource reclaimed client.
         ClientProfile profile = getClientProfile(reclaimingClientId);
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 584530c..8b80b4a 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -687,6 +687,7 @@
         mUnderlyingNetworkController =
                 mDeps.newUnderlyingNetworkController(
                         mVcnContext,
+                        mConnectionConfig,
                         subscriptionGroup,
                         mLastSnapshot,
                         mUnderlyingNetworkControllerCallback);
@@ -2376,11 +2377,12 @@
         /** Builds a new UnderlyingNetworkController. */
         public UnderlyingNetworkController newUnderlyingNetworkController(
                 VcnContext vcnContext,
+                VcnGatewayConnectionConfig connectionConfig,
                 ParcelUuid subscriptionGroup,
                 TelephonySubscriptionSnapshot snapshot,
                 UnderlyingNetworkControllerCallback callback) {
             return new UnderlyingNetworkController(
-                    vcnContext, subscriptionGroup, snapshot, callback);
+                    vcnContext, connectionConfig, subscriptionGroup, snapshot, callback);
         }
 
         /** Builds a new IkeSession. */
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
index bea8ae9..7b26fe0 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
@@ -15,25 +15,36 @@
  */
 package com.android.server.vcn.routeselection;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.vcn.VcnUnderlyingNetworkPriority.NETWORK_QUALITY_ANY;
+import static android.net.vcn.VcnUnderlyingNetworkPriority.NETWORK_QUALITY_OK;
 
 import static com.android.server.VcnManagementService.LOCAL_LOG;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnCellUnderlyingNetworkPriority;
 import android.net.vcn.VcnManager;
+import android.net.vcn.VcnUnderlyingNetworkPriority;
+import android.net.vcn.VcnWifiUnderlyingNetworkPriority;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
 
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 /** @hide */
@@ -56,52 +67,20 @@
      */
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     static final int WIFI_EXIT_RSSI_THRESHOLD_DEFAULT = -74;
-    /** Priority for any cellular network for which the subscription is listed as opportunistic */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    static final int PRIORITY_OPPORTUNISTIC_CELLULAR = 0;
-    /** Priority for any WiFi network which is in use, and satisfies the in-use RSSI threshold */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    static final int PRIORITY_WIFI_IN_USE = 1;
-    /** Priority for any WiFi network which satisfies the prospective-network RSSI threshold */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    static final int PRIORITY_WIFI_PROSPECTIVE = 2;
-    /** Priority for any standard macro cellular network */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    static final int PRIORITY_MACRO_CELLULAR = 3;
+
     /** Priority for any other networks (including unvalidated, etc) */
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     static final int PRIORITY_ANY = Integer.MAX_VALUE;
 
-    private static final SparseArray<String> PRIORITY_TO_STRING_MAP = new SparseArray<>();
-
-    static {
-        PRIORITY_TO_STRING_MAP.put(
-                PRIORITY_OPPORTUNISTIC_CELLULAR, "PRIORITY_OPPORTUNISTIC_CELLULAR");
-        PRIORITY_TO_STRING_MAP.put(PRIORITY_WIFI_IN_USE, "PRIORITY_WIFI_IN_USE");
-        PRIORITY_TO_STRING_MAP.put(PRIORITY_WIFI_PROSPECTIVE, "PRIORITY_WIFI_PROSPECTIVE");
-        PRIORITY_TO_STRING_MAP.put(PRIORITY_MACRO_CELLULAR, "PRIORITY_MACRO_CELLULAR");
-        PRIORITY_TO_STRING_MAP.put(PRIORITY_ANY, "PRIORITY_ANY");
-    }
-
-    /**
-     * Gives networks a priority class, based on the following priorities:
-     *
-     * <ol>
-     *   <li>Opportunistic cellular
-     *   <li>Carrier WiFi, signal strength >= WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT
-     *   <li>Carrier WiFi, active network + signal strength >= WIFI_EXIT_RSSI_THRESHOLD_DEFAULT
-     *   <li>Macro cellular
-     *   <li>Any others
-     * </ol>
-     */
-    static int calculatePriorityClass(
+    /** Gives networks a priority class, based on configured VcnGatewayConnectionConfig */
+    public static int calculatePriorityClass(
+            VcnContext vcnContext,
             UnderlyingNetworkRecord networkRecord,
+            LinkedHashSet<VcnUnderlyingNetworkPriority> underlyingNetworkPriorities,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
             PersistableBundle carrierConfig) {
-        final NetworkCapabilities caps = networkRecord.networkCapabilities;
-
         // mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
 
         if (networkRecord.isBlocked) {
@@ -109,8 +88,167 @@
             return PRIORITY_ANY;
         }
 
-        if (caps.hasTransport(TRANSPORT_CELLULAR)
-                && isOpportunistic(snapshot, caps.getSubscriptionIds())) {
+        if (snapshot == null) {
+            logWtf("Got null snapshot");
+            return PRIORITY_ANY;
+        }
+
+        int priorityIndex = 0;
+        for (VcnUnderlyingNetworkPriority nwPriority : underlyingNetworkPriorities) {
+            if (checkMatchesPriorityRule(
+                    vcnContext,
+                    nwPriority,
+                    networkRecord,
+                    subscriptionGroup,
+                    snapshot,
+                    currentlySelected,
+                    carrierConfig)) {
+                return priorityIndex;
+            }
+            priorityIndex++;
+        }
+        return PRIORITY_ANY;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static boolean checkMatchesPriorityRule(
+            VcnContext vcnContext,
+            VcnUnderlyingNetworkPriority networkPriority,
+            UnderlyingNetworkRecord networkRecord,
+            ParcelUuid subscriptionGroup,
+            TelephonySubscriptionSnapshot snapshot,
+            UnderlyingNetworkRecord currentlySelected,
+            PersistableBundle carrierConfig) {
+        // TODO: Check Network Quality reported by metric monitors/probers.
+
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+        if (!networkPriority.allowMetered() && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)) {
+            return false;
+        }
+
+        if (vcnContext.isInTestMode() && caps.hasTransport(TRANSPORT_TEST)) {
+            return true;
+        }
+
+        if (networkPriority instanceof VcnWifiUnderlyingNetworkPriority) {
+            return checkMatchesWifiPriorityRule(
+                    (VcnWifiUnderlyingNetworkPriority) networkPriority,
+                    networkRecord,
+                    currentlySelected,
+                    carrierConfig);
+        }
+
+        if (networkPriority instanceof VcnCellUnderlyingNetworkPriority) {
+            return checkMatchesCellPriorityRule(
+                    vcnContext,
+                    (VcnCellUnderlyingNetworkPriority) networkPriority,
+                    networkRecord,
+                    subscriptionGroup,
+                    snapshot);
+        }
+
+        logWtf(
+                "Got unknown VcnUnderlyingNetworkPriority class: "
+                        + networkPriority.getClass().getSimpleName());
+        return false;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static boolean checkMatchesWifiPriorityRule(
+            VcnWifiUnderlyingNetworkPriority networkPriority,
+            UnderlyingNetworkRecord networkRecord,
+            UnderlyingNetworkRecord currentlySelected,
+            PersistableBundle carrierConfig) {
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+
+        if (!caps.hasTransport(TRANSPORT_WIFI)) {
+            return false;
+        }
+
+        // TODO: Move the Network Quality check to the network metric monitor framework.
+        if (networkPriority.getNetworkQuality()
+                > getWifiQuality(networkRecord, currentlySelected, carrierConfig)) {
+            return false;
+        }
+
+        if (networkPriority.getSsid() != null && networkPriority.getSsid() != caps.getSsid()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private static int getWifiQuality(
+            UnderlyingNetworkRecord networkRecord,
+            UnderlyingNetworkRecord currentlySelected,
+            PersistableBundle carrierConfig) {
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+        final boolean isSelectedNetwork =
+                currentlySelected != null
+                        && networkRecord.network.equals(currentlySelected.network);
+
+        if (isSelectedNetwork
+                && caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)) {
+            return NETWORK_QUALITY_OK;
+        }
+
+        if (caps.getSignalStrength() >= getWifiEntryRssiThreshold(carrierConfig)) {
+            return NETWORK_QUALITY_OK;
+        }
+
+        return NETWORK_QUALITY_ANY;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static boolean checkMatchesCellPriorityRule(
+            VcnContext vcnContext,
+            VcnCellUnderlyingNetworkPriority networkPriority,
+            UnderlyingNetworkRecord networkRecord,
+            ParcelUuid subscriptionGroup,
+            TelephonySubscriptionSnapshot snapshot) {
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+
+        if (!caps.hasTransport(TRANSPORT_CELLULAR)) {
+            return false;
+        }
+
+        final TelephonyNetworkSpecifier telephonyNetworkSpecifier =
+                ((TelephonyNetworkSpecifier) caps.getNetworkSpecifier());
+        if (telephonyNetworkSpecifier == null) {
+            logWtf("Got null NetworkSpecifier");
+            return false;
+        }
+
+        final int subId = telephonyNetworkSpecifier.getSubscriptionId();
+        final TelephonyManager subIdSpecificTelephonyMgr =
+                vcnContext
+                        .getContext()
+                        .getSystemService(TelephonyManager.class)
+                        .createForSubscriptionId(subId);
+
+        if (!networkPriority.getAllowedPlmnIds().isEmpty()) {
+            final String plmnId = subIdSpecificTelephonyMgr.getNetworkOperator();
+            if (!networkPriority.getAllowedPlmnIds().contains(plmnId)) {
+                return false;
+            }
+        }
+
+        if (!networkPriority.getAllowedSpecificCarrierIds().isEmpty()) {
+            final int carrierId = subIdSpecificTelephonyMgr.getSimSpecificCarrierId();
+            if (!networkPriority.getAllowedSpecificCarrierIds().contains(carrierId)) {
+                return false;
+            }
+        }
+
+        if (!networkPriority.allowRoaming() && !caps.hasCapability(NET_CAPABILITY_NOT_ROAMING)) {
+            return false;
+        }
+
+        if (networkPriority.requireOpportunistic()) {
+            if (!isOpportunistic(snapshot, caps.getSubscriptionIds())) {
+                return false;
+            }
+
             // If this carrier is the active data provider, ensure that opportunistic is only
             // ever prioritized if it is also the active data subscription. This ensures that
             // if an opportunistic subscription is still in the process of being switched to,
@@ -121,36 +259,15 @@
             // Allow the following two cases:
             // 1. Active subId is NOT in the group that this VCN is supporting
             // 2. This opportunistic subscription is for the active subId
-            if (!snapshot.getAllSubIdsInGroup(subscriptionGroup)
+            if (snapshot.getAllSubIdsInGroup(subscriptionGroup)
                             .contains(SubscriptionManager.getActiveDataSubscriptionId())
-                    || caps.getSubscriptionIds()
+                    && !caps.getSubscriptionIds()
                             .contains(SubscriptionManager.getActiveDataSubscriptionId())) {
-                return PRIORITY_OPPORTUNISTIC_CELLULAR;
+                return false;
             }
         }
 
-        if (caps.hasTransport(TRANSPORT_WIFI)) {
-            if (caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)
-                    && currentlySelected != null
-                    && networkRecord.network.equals(currentlySelected.network)) {
-                return PRIORITY_WIFI_IN_USE;
-            }
-
-            if (caps.getSignalStrength() >= getWifiEntryRssiThreshold(carrierConfig)) {
-                return PRIORITY_WIFI_PROSPECTIVE;
-            }
-        }
-
-        // Disallow opportunistic subscriptions from matching PRIORITY_MACRO_CELLULAR, as might
-        // be the case when Default Data SubId (CBRS) != Active Data SubId (MACRO), as might be
-        // the case if the Default Data SubId does not support certain services (eg voice
-        // calling)
-        if (caps.hasTransport(TRANSPORT_CELLULAR)
-                && !isOpportunistic(snapshot, caps.getSubscriptionIds())) {
-            return PRIORITY_MACRO_CELLULAR;
-        }
-
-        return PRIORITY_ANY;
+        return true;
     }
 
     static boolean isOpportunistic(
@@ -185,10 +302,6 @@
         return WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
     }
 
-    static String priorityClassToString(int priorityClass) {
-        return PRIORITY_TO_STRING_MAP.get(priorityClass, "unknown");
-    }
-
     private static void logWtf(String msg) {
         Slog.wtf(TAG, msg);
         LOCAL_LOG.log(TAG + " WTF: " + msg);
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 071c7a6..cd124ee 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -32,6 +32,8 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnGatewayConnectionConfig;
+import android.net.vcn.VcnUnderlyingNetworkPriority;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.ParcelUuid;
@@ -68,6 +70,7 @@
     @NonNull private static final String TAG = UnderlyingNetworkController.class.getSimpleName();
 
     @NonNull private final VcnContext mVcnContext;
+    @NonNull private final VcnGatewayConnectionConfig mConnectionConfig;
     @NonNull private final ParcelUuid mSubscriptionGroup;
     @NonNull private final UnderlyingNetworkControllerCallback mCb;
     @NonNull private final Dependencies mDeps;
@@ -91,24 +94,22 @@
 
     public UnderlyingNetworkController(
             @NonNull VcnContext vcnContext,
+            @NonNull VcnGatewayConnectionConfig connectionConfig,
             @NonNull ParcelUuid subscriptionGroup,
             @NonNull TelephonySubscriptionSnapshot snapshot,
             @NonNull UnderlyingNetworkControllerCallback cb) {
-        this(
-                vcnContext,
-                subscriptionGroup,
-                snapshot,
-                cb,
-                new Dependencies());
+        this(vcnContext, connectionConfig, subscriptionGroup, snapshot, cb, new Dependencies());
     }
 
     private UnderlyingNetworkController(
             @NonNull VcnContext vcnContext,
+            @NonNull VcnGatewayConnectionConfig connectionConfig,
             @NonNull ParcelUuid subscriptionGroup,
             @NonNull TelephonySubscriptionSnapshot snapshot,
             @NonNull UnderlyingNetworkControllerCallback cb,
             @NonNull Dependencies deps) {
         mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
+        mConnectionConfig = Objects.requireNonNull(connectionConfig, "Missing connectionConfig");
         mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
         mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot");
         mCb = Objects.requireNonNull(cb, "Missing cb");
@@ -399,6 +400,8 @@
             TreeSet<UnderlyingNetworkRecord> sorted =
                     new TreeSet<>(
                             UnderlyingNetworkRecord.getComparator(
+                                    mVcnContext,
+                                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
                                     mSubscriptionGroup,
                                     mLastSnapshot,
                                     mCurrentRecord,
@@ -495,12 +498,31 @@
         pw.println(
                 "Currently selected: " + (mCurrentRecord == null ? null : mCurrentRecord.network));
 
+        pw.println("VcnUnderlyingNetworkPriority list:");
+        pw.increaseIndent();
+        int index = 0;
+        for (VcnUnderlyingNetworkPriority priority :
+                mConnectionConfig.getVcnUnderlyingNetworkPriorities()) {
+            pw.println("Priority index: " + index);
+            priority.dump(pw);
+            index++;
+        }
+        pw.decreaseIndent();
+        pw.println();
+
         pw.println("Underlying networks:");
         pw.increaseIndent();
         if (mRouteSelectionCallback != null) {
             for (UnderlyingNetworkRecord record :
                     mRouteSelectionCallback.getSortedUnderlyingNetworks()) {
-                record.dump(pw, mSubscriptionGroup, mLastSnapshot, mCurrentRecord, mCarrierConfig);
+                record.dump(
+                        mVcnContext,
+                        pw,
+                        mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                        mSubscriptionGroup,
+                        mLastSnapshot,
+                        mCurrentRecord,
+                        mCarrierConfig);
             }
         }
         pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
index 65c69de..27ba854 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
@@ -21,6 +21,7 @@
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.vcn.VcnUnderlyingNetworkPriority;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
 
@@ -28,8 +29,10 @@
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
 
 import java.util.Comparator;
+import java.util.LinkedHashSet;
 import java.util.Objects;
 
 /**
@@ -73,22 +76,64 @@
     }
 
     static Comparator<UnderlyingNetworkRecord> getComparator(
+            VcnContext vcnContext,
+            LinkedHashSet<VcnUnderlyingNetworkPriority> underlyingNetworkPriorities,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
             PersistableBundle carrierConfig) {
         return (left, right) -> {
-            return Integer.compare(
+            final int leftIndex =
                     NetworkPriorityClassifier.calculatePriorityClass(
-                            left, subscriptionGroup, snapshot, currentlySelected, carrierConfig),
+                            vcnContext,
+                            left,
+                            underlyingNetworkPriorities,
+                            subscriptionGroup,
+                            snapshot,
+                            currentlySelected,
+                            carrierConfig);
+            final int rightIndex =
                     NetworkPriorityClassifier.calculatePriorityClass(
-                            right, subscriptionGroup, snapshot, currentlySelected, carrierConfig));
+                            vcnContext,
+                            right,
+                            underlyingNetworkPriorities,
+                            subscriptionGroup,
+                            snapshot,
+                            currentlySelected,
+                            carrierConfig);
+
+            // In the case of networks in the same priority class, prioritize based on other
+            // criteria (eg. actively selected network, link metrics, etc)
+            if (leftIndex == rightIndex) {
+                // TODO: Improve the strategy of network selection when both UnderlyingNetworkRecord
+                // fall into the same priority class.
+                if (isSelected(left, currentlySelected)) {
+                    return -1;
+                }
+                if (isSelected(left, currentlySelected)) {
+                    return 1;
+                }
+            }
+            return Integer.compare(leftIndex, rightIndex);
         };
     }
 
+    private static boolean isSelected(
+            UnderlyingNetworkRecord recordToCheck, UnderlyingNetworkRecord currentlySelected) {
+        if (currentlySelected == null) {
+            return false;
+        }
+        if (currentlySelected.network == recordToCheck.network) {
+            return true;
+        }
+        return false;
+    }
+
     /** Dumps the state of this record for logging and debugging purposes. */
     void dump(
+            VcnContext vcnContext,
             IndentingPrintWriter pw,
+            LinkedHashSet<VcnUnderlyingNetworkPriority> underlyingNetworkPriorities,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
@@ -96,15 +141,17 @@
         pw.println("UnderlyingNetworkRecord:");
         pw.increaseIndent();
 
-        final int priorityClass =
+        final int priorityIndex =
                 NetworkPriorityClassifier.calculatePriorityClass(
-                        this, subscriptionGroup, snapshot, currentlySelected, carrierConfig);
-        pw.println(
-                "Priority class: "
-                        + NetworkPriorityClassifier.priorityClassToString(priorityClass)
-                        + " ("
-                        + priorityClass
-                        + ")");
+                        vcnContext,
+                        this,
+                        underlyingNetworkPriorities,
+                        subscriptionGroup,
+                        snapshot,
+                        currentlySelected,
+                        carrierConfig);
+
+        pw.println("Priority index:" + priorityIndex);
         pw.println("mNetwork: " + network);
         pw.println("mNetworkCapabilities: " + networkCapabilities);
         pw.println("mLinkProperties: " + linkProperties);
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index 03d6590..d5abe4f 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -1426,21 +1426,21 @@
     }
 
     @TransitionType int getKeyguardTransition() {
-        // In case we unocclude Keyguard and occlude it again, meaning that we never actually
-        // unoccclude/occlude Keyguard, but just run a normal transition.
-        final int occludeIndex = mNextAppTransitionRequests.indexOf(TRANSIT_KEYGUARD_UNOCCLUDE);
-        if (occludeIndex != -1
-                && occludeIndex < mNextAppTransitionRequests.indexOf(TRANSIT_KEYGUARD_OCCLUDE)) {
+        if (mNextAppTransitionRequests.indexOf(TRANSIT_KEYGUARD_GOING_AWAY) != -1) {
+            return TRANSIT_KEYGUARD_GOING_AWAY;
+        }
+        final int unoccludeIndex = mNextAppTransitionRequests.indexOf(TRANSIT_KEYGUARD_UNOCCLUDE);
+        final int occludeIndex = mNextAppTransitionRequests.indexOf(TRANSIT_KEYGUARD_OCCLUDE);
+        // No keyguard related transition requests.
+        if (unoccludeIndex == -1 && occludeIndex == -1) {
             return TRANSIT_NONE;
         }
-
-        for (int i = 0; i < mNextAppTransitionRequests.size(); ++i) {
-            final @TransitionType int transit = mNextAppTransitionRequests.get(i);
-            if (isKeyguardTransit(transit)) {
-                return transit;
-            }
+        // In case we unocclude Keyguard and occlude it again, meaning that we never actually
+        // unoccclude/occlude Keyguard, but just run a normal transition.
+        if (unoccludeIndex != -1 && unoccludeIndex < occludeIndex) {
+            return TRANSIT_NONE;
         }
-        return TRANSIT_NONE;
+        return unoccludeIndex != -1 ? TRANSIT_KEYGUARD_UNOCCLUDE : TRANSIT_KEYGUARD_OCCLUDE;
     }
 
     @TransitionType int getFirstAppTransition() {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 5eabe75..fdaa2fc 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2187,14 +2187,13 @@
     TaskFragmentInfo getTaskFragmentInfo() {
         List<IBinder> childActivities = new ArrayList<>();
         for (int i = 0; i < getChildCount(); i++) {
-            WindowContainer wc = getChildAt(i);
-            if (mTaskFragmentOrganizerUid != INVALID_UID
-                    && wc.asActivityRecord() != null
-                    && wc.asActivityRecord().info.processName.equals(
-                            mTaskFragmentOrganizerProcessName)
-                    && wc.asActivityRecord().getUid() == mTaskFragmentOrganizerUid) {
+            final WindowContainer wc = getChildAt(i);
+            final ActivityRecord ar = wc.asActivityRecord();
+            if (mTaskFragmentOrganizerUid != INVALID_UID && ar != null
+                    && ar.info.processName.equals(mTaskFragmentOrganizerProcessName)
+                    && ar.getUid() == mTaskFragmentOrganizerUid && !ar.finishing) {
                 // Only includes Activities that belong to the organizer process for security.
-                childActivities.add(wc.asActivityRecord().token);
+                childActivities.add(ar.token);
             }
         }
         final Point positionInParent = new Point();
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 29c27f9..c7fdefc 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -24,6 +24,7 @@
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.content.res.Configuration;
+import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -579,4 +580,26 @@
                         event.mException);
         }
     }
+
+    // TODO(b/204399167): change to push the embedded state to the client side
+    @Override
+    public boolean isActivityEmbedded(IBinder activityToken) {
+        synchronized (mGlobalLock) {
+            final ActivityRecord activity = ActivityRecord.forTokenLocked(activityToken);
+            if (activity == null) {
+                return false;
+            }
+            final TaskFragment taskFragment = activity.getOrganizedTaskFragment();
+            if (taskFragment == null) {
+                return false;
+            }
+            final Task parentTask = taskFragment.getTask();
+            if (parentTask != null) {
+                final Rect taskBounds = parentTask.getBounds();
+                final Rect taskFragBounds = taskFragment.getBounds();
+                return !taskBounds.equals(taskFragBounds) && taskBounds.contains(taskFragBounds);
+            }
+            return false;
+        }
+    }
 }
diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
index 4504853..06f5aed 100644
--- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
+++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
@@ -37,10 +37,7 @@
 #include <android/hardware/gnss/measurement_corrections/1.0/IMeasurementCorrections.h>
 #include <android/hardware/gnss/measurement_corrections/1.1/IMeasurementCorrections.h>
 #include <android/hardware/gnss/visibility_control/1.0/IGnssVisibilityControl.h>
-#include <arpa/inet.h>
 #include <binder/IServiceManager.h>
-#include <linux/in.h>
-#include <linux/in6.h>
 #include <nativehelper/JNIHelp.h>
 #include <pthread.h>
 #include <string.h>
@@ -52,6 +49,7 @@
 
 #include "android_runtime/AndroidRuntime.h"
 #include "android_runtime/Log.h"
+#include "gnss/AGnss.h"
 #include "gnss/GnssAntennaInfoCallback.h"
 #include "gnss/GnssBatching.h"
 #include "gnss/GnssConfiguration.h"
@@ -69,7 +67,6 @@
 static jmethodID method_reportLocation;
 static jmethodID method_reportStatus;
 static jmethodID method_reportSvStatus;
-static jmethodID method_reportAGpsStatus;
 static jmethodID method_reportNmea;
 static jmethodID method_setTopHalCapabilities;
 static jmethodID method_setGnssYearOfHardware;
@@ -163,10 +160,6 @@
 using IGnssAntennaInfo = android::hardware::gnss::V2_1::IGnssAntennaInfo;
 using IAGnssRil_V1_0 = android::hardware::gnss::V1_0::IAGnssRil;
 using IAGnssRil_V2_0 = android::hardware::gnss::V2_0::IAGnssRil;
-using IAGnss_V1_0 = android::hardware::gnss::V1_0::IAGnss;
-using IAGnss_V2_0 = android::hardware::gnss::V2_0::IAGnss;
-using IAGnssCallback_V1_0 = android::hardware::gnss::V1_0::IAGnssCallback;
-using IAGnssCallback_V2_0 = android::hardware::gnss::V2_0::IAGnssCallback;
 
 using IMeasurementCorrections_V1_0 = android::hardware::gnss::measurement_corrections::V1_0::IMeasurementCorrections;
 using IMeasurementCorrections_V1_1 = android::hardware::gnss::measurement_corrections::V1_1::IMeasurementCorrections;
@@ -182,6 +175,7 @@
 using android::hardware::gnss::IGnssPowerIndication;
 using android::hardware::gnss::IGnssPowerIndicationCallback;
 using android::hardware::gnss::PsdsType;
+using IAGnssAidl = android::hardware::gnss::IAGnss;
 using IGnssAidl = android::hardware::gnss::IGnss;
 using IGnssCallbackAidl = android::hardware::gnss::IGnssCallback;
 using IGnssBatchingAidl = android::hardware::gnss::IGnssBatching;
@@ -215,8 +209,6 @@
 sp<IGnssXtra> gnssXtraIface = nullptr;
 sp<IAGnssRil_V1_0> agnssRilIface = nullptr;
 sp<IAGnssRil_V2_0> agnssRilIface_V2_0 = nullptr;
-sp<IAGnss_V1_0> agnssIface = nullptr;
-sp<IAGnss_V2_0> agnssIface_V2_0 = nullptr;
 sp<IGnssDebug_V1_0> gnssDebugIface = nullptr;
 sp<IGnssDebug_V2_0> gnssDebugIface_V2_0 = nullptr;
 sp<IGnssNi> gnssNiIface = nullptr;
@@ -231,6 +223,7 @@
 std::unique_ptr<android::gnss::GnssNavigationMessageInterface> gnssNavigationMessageIface = nullptr;
 std::unique_ptr<android::gnss::GnssBatchingInterface> gnssBatchingIface = nullptr;
 std::unique_ptr<android::gnss::GnssGeofenceInterface> gnssGeofencingIface = nullptr;
+std::unique_ptr<android::gnss::AGnssInterface> agnssIface = nullptr;
 
 #define WAKE_LOCK_NAME  "GPS"
 
@@ -255,34 +248,6 @@
     return value ? JNI_TRUE : JNI_FALSE;
 }
 
-struct ScopedJniString {
-    ScopedJniString(JNIEnv* env, jstring javaString) : mEnv(env), mJavaString(javaString) {
-        mNativeString = mEnv->GetStringUTFChars(mJavaString, nullptr);
-    }
-
-    ~ScopedJniString() {
-        if (mNativeString != nullptr) {
-            mEnv->ReleaseStringUTFChars(mJavaString, mNativeString);
-        }
-    }
-
-    const char* c_str() const {
-        return mNativeString;
-    }
-
-    operator hidl_string() const {
-        return hidl_string(mNativeString);
-    }
-
-private:
-    ScopedJniString(const ScopedJniString&) = delete;
-    ScopedJniString& operator=(const ScopedJniString&) = delete;
-
-    JNIEnv* mEnv;
-    jstring mJavaString;
-    const char* mNativeString;
-};
-
 static GnssLocation_V1_0 createGnssLocation_V1_0(
         jint gnssLocationFlags, jdouble latitudeDegrees, jdouble longitudeDegrees,
         jdouble altitudeMeters, jfloat speedMetersPerSec, jfloat bearingDegrees,
@@ -806,122 +771,6 @@
 }
 
 /*
- * AGnssCallback_V1_0 implements callback methods required by the IAGnssCallback 1.0 interface.
- */
-struct AGnssCallback_V1_0 : public IAGnssCallback_V1_0 {
-    // Methods from ::android::hardware::gps::V1_0::IAGnssCallback follow.
-    Return<void> agnssStatusIpV6Cb(
-      const IAGnssCallback_V1_0::AGnssStatusIpV6& agps_status) override;
-
-    Return<void> agnssStatusIpV4Cb(
-      const IAGnssCallback_V1_0::AGnssStatusIpV4& agps_status) override;
- private:
-    jbyteArray convertToIpV4(uint32_t ip);
-};
-
-Return<void> AGnssCallback_V1_0::agnssStatusIpV6Cb(
-        const IAGnssCallback_V1_0::AGnssStatusIpV6& agps_status) {
-    JNIEnv* env = getJniEnv();
-    jbyteArray byteArray = nullptr;
-
-    byteArray = env->NewByteArray(16);
-    if (byteArray != nullptr) {
-        env->SetByteArrayRegion(byteArray, 0, 16,
-                                (const jbyte*)(agps_status.ipV6Addr.data()));
-    } else {
-        ALOGE("Unable to allocate byte array for IPv6 address.");
-    }
-
-    IF_ALOGD() {
-        // log the IP for reference in case there is a bogus value pushed by HAL
-        char str[INET6_ADDRSTRLEN];
-        inet_ntop(AF_INET6, agps_status.ipV6Addr.data(), str, INET6_ADDRSTRLEN);
-        ALOGD("AGPS IP is v6: %s", str);
-    }
-
-    jsize byteArrayLength = byteArray != nullptr ? env->GetArrayLength(byteArray) : 0;
-    ALOGV("Passing AGPS IP addr: size %d", byteArrayLength);
-    env->CallVoidMethod(mCallbacksObj, method_reportAGpsStatus,
-                        agps_status.type, agps_status.status, byteArray);
-
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-
-    if (byteArray) {
-        env->DeleteLocalRef(byteArray);
-    }
-
-    return Void();
-}
-
-Return<void> AGnssCallback_V1_0::agnssStatusIpV4Cb(
-        const IAGnssCallback_V1_0::AGnssStatusIpV4& agps_status) {
-    JNIEnv* env = getJniEnv();
-    jbyteArray byteArray = nullptr;
-
-    uint32_t ipAddr = agps_status.ipV4Addr;
-    byteArray = convertToIpV4(ipAddr);
-
-    IF_ALOGD() {
-        /*
-         * log the IP for reference in case there is a bogus value pushed by
-         * HAL.
-         */
-        char str[INET_ADDRSTRLEN];
-        inet_ntop(AF_INET, &ipAddr, str, INET_ADDRSTRLEN);
-        ALOGD("AGPS IP is v4: %s", str);
-    }
-
-    jsize byteArrayLength =
-      byteArray != nullptr ? env->GetArrayLength(byteArray) : 0;
-    ALOGV("Passing AGPS IP addr: size %d", byteArrayLength);
-    env->CallVoidMethod(mCallbacksObj, method_reportAGpsStatus,
-                      agps_status.type, agps_status.status, byteArray);
-
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-
-    if (byteArray) {
-        env->DeleteLocalRef(byteArray);
-    }
-    return Void();
-}
-
-jbyteArray AGnssCallback_V1_0::convertToIpV4(uint32_t ip) {
-    if (INADDR_NONE == ip) {
-        return nullptr;
-    }
-
-    JNIEnv* env = getJniEnv();
-    jbyteArray byteArray = env->NewByteArray(4);
-    if (byteArray == nullptr) {
-        ALOGE("Unable to allocate byte array for IPv4 address");
-        return nullptr;
-    }
-
-    jbyte ipv4[4];
-    ALOGV("Converting IPv4 address byte array (net_order) %x", ip);
-    memcpy(ipv4, &ip, sizeof(ipv4));
-    env->SetByteArrayRegion(byteArray, 0, 4, (const jbyte*)ipv4);
-    return byteArray;
-}
-
-/*
- * AGnssCallback_V2_0 implements callback methods required by the IAGnssCallback 2.0 interface.
- */
-struct AGnssCallback_V2_0 : public IAGnssCallback_V2_0 {
-    // Methods from ::android::hardware::gps::V2_0::IAGnssCallback follow.
-    Return<void> agnssStatusCb(IAGnssCallback_V2_0::AGnssType type,
-        IAGnssCallback_V2_0::AGnssStatusValue status) override;
-};
-
-Return<void> AGnssCallback_V2_0::agnssStatusCb(IAGnssCallback_V2_0::AGnssType type,
-        IAGnssCallback_V2_0::AGnssStatusValue status) {
-    JNIEnv* env = getJniEnv();
-    env->CallVoidMethod(mCallbacksObj, method_reportAGpsStatus, type, status, nullptr);
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-    return Void();
-}
-
-/*
  * AGnssRilCallback implements the callback methods required by the AGnssRil
  * interface.
  */
@@ -990,7 +839,6 @@
             "(ZLandroid/location/Location;)V");
     method_reportStatus = env->GetMethodID(clazz, "reportStatus", "(I)V");
     method_reportSvStatus = env->GetMethodID(clazz, "reportSvStatus", "(I[I[F[F[F[F[F)V");
-    method_reportAGpsStatus = env->GetMethodID(clazz, "reportAGpsStatus", "(II[B)V");
     method_reportNmea = env->GetMethodID(clazz, "reportNmea", "(J)V");
     method_setTopHalCapabilities = env->GetMethodID(clazz, "setTopHalCapabilities", "(I)V");
     method_setGnssYearOfHardware = env->GetMethodID(clazz, "setGnssYearOfHardware", "(I)V");
@@ -1075,8 +923,10 @@
     gnss::GnssAntennaInfo_class_init_once(env, clazz);
     gnss::GnssBatching_class_init_once(env, clazz);
     gnss::GnssConfiguration_class_init_once(env);
+    gnss::GnssGeofence_class_init_once(env, clazz);
     gnss::GnssMeasurement_class_init_once(env, clazz);
     gnss::GnssNavigationMessage_class_init_once(env, clazz);
+    gnss::AGnss_class_init_once(env, clazz);
     gnss::Utils_class_init_once(env);
 }
 
@@ -1147,19 +997,21 @@
         }
     }
 
-    if (gnssHal_V2_0 != nullptr) {
+    if (gnssHalAidl != nullptr) {
+        sp<IAGnssAidl> agnssAidl;
+        auto status = gnssHalAidl->getExtensionAGnss(&agnssAidl);
+        if (checkAidlStatus(status, "Unable to get a handle to AGnss interface.")) {
+            agnssIface = std::make_unique<gnss::AGnss>(agnssAidl);
+        }
+    } else if (gnssHal_V2_0 != nullptr) {
         auto agnss_V2_0 = gnssHal_V2_0->getExtensionAGnss_2_0();
-        if (!agnss_V2_0.isOk()) {
-            ALOGD("Unable to get a handle to AGnss_V2_0");
-        } else {
-            agnssIface_V2_0 = agnss_V2_0;
+        if (checkHidlReturn(agnss_V2_0, "Unable to get a handle to AGnss_V2_0")) {
+            agnssIface = std::make_unique<gnss::AGnss_V2_0>(agnss_V2_0);
         }
     } else if (gnssHal != nullptr) {
         auto agnss_V1_0 = gnssHal->getExtensionAGnss();
-        if (!agnss_V1_0.isOk()) {
-            ALOGD("Unable to get a handle to AGnss");
-        } else {
-            agnssIface = agnss_V1_0;
+        if (checkHidlReturn(agnss_V1_0, "Unable to get a handle to AGnss_V1_0")) {
+            agnssIface = std::make_unique<gnss::AGnss_V1_0>(agnss_V1_0);
         }
     }
 
@@ -1452,16 +1304,9 @@
         }
     }
 
-    // Set IAGnss.hal callback.
-    if (agnssIface_V2_0 != nullptr) {
-        sp<IAGnssCallback_V2_0> aGnssCbIface = new AGnssCallback_V2_0();
-        auto agnssStatus = agnssIface_V2_0->setCallback(aGnssCbIface);
-        checkHidlReturn(agnssStatus, "IAGnss 2.0 setCallback() failed.");
-    } else if (agnssIface != nullptr) {
-        sp<IAGnssCallback_V1_0> aGnssCbIface = new AGnssCallback_V1_0();
-        auto agnssStatus = agnssIface->setCallback(aGnssCbIface);
-        checkHidlReturn(agnssStatus, "IAGnss setCallback() failed.");
-    } else {
+    // Set IAGnss callback.
+    if (agnssIface == nullptr ||
+        !agnssIface->setCallback(std::make_unique<gnss::AGnssCallback>())) {
         ALOGI("Unable to initialize IAGnss interface.");
     }
 
@@ -1737,61 +1582,6 @@
     env->ReleasePrimitiveArrayCritical(data, bytes, JNI_ABORT);
 }
 
-struct AGnssDispatcher {
-    static void dataConnOpen(sp<IAGnss_V1_0> agnssIface, JNIEnv* env, jstring apn, jint apnIpType);
-    static void dataConnOpen(sp<IAGnss_V2_0> agnssIface_V2_0, JNIEnv* env, jlong networkHandle,
-            jstring apn, jint apnIpType);
-
-    template <class T>
-    static void dataConnClosed(sp<T> agnssIface);
-
-    template <class T>
-    static void dataConnFailed(sp<T> agnssIface);
-
-    template <class T, class U>
-    static void setServer(sp<T> agnssIface, JNIEnv* env, jint type, jstring hostname, jint port);
-
-private:
-    AGnssDispatcher() = delete;
-};
-
-void AGnssDispatcher::dataConnOpen(sp<IAGnss_V1_0> agnssIface, JNIEnv* env, jstring apn,
-        jint apnIpType) {
-    ScopedJniString jniApn{env, apn};
-    auto result = agnssIface->dataConnOpen(jniApn,
-            static_cast<IAGnss_V1_0::ApnIpType>(apnIpType));
-    checkHidlReturn(result, "IAGnss dataConnOpen() failed. APN and its IP type not set.");
-}
-
-void AGnssDispatcher::dataConnOpen(sp<IAGnss_V2_0> agnssIface_V2_0, JNIEnv* env,
-        jlong networkHandle, jstring apn, jint apnIpType) {
-    ScopedJniString jniApn{env, apn};
-    auto result = agnssIface_V2_0->dataConnOpen(static_cast<uint64_t>(networkHandle), jniApn,
-            static_cast<IAGnss_V2_0::ApnIpType>(apnIpType));
-    checkHidlReturn(result, "IAGnss 2.0 dataConnOpen() failed. APN and its IP type not set.");
-}
-
-template<class T>
-void AGnssDispatcher::dataConnClosed(sp<T> agnssIface) {
-    auto result = agnssIface->dataConnClosed();
-    checkHidlReturn(result, "IAGnss dataConnClosed() failed.");
-}
-
-template<class T>
-void AGnssDispatcher::dataConnFailed(sp<T> agnssIface) {
-    auto result = agnssIface->dataConnFailed();
-    checkHidlReturn(result, "IAGnss dataConnFailed() failed.");
-}
-
-template <class T, class U>
-void AGnssDispatcher::setServer(sp<T> agnssIface, JNIEnv* env, jint type, jstring hostname,
-        jint port) {
-    ScopedJniString jniHostName{env, hostname};
-    auto result = agnssIface->setServer(static_cast<typename U::AGnssType>(type),
-            jniHostName, port);
-    checkHidlReturn(result, "IAGnss setServer() failed. Host name and port not set.");
-}
-
 static void android_location_GnssNetworkConnectivityHandler_agps_data_conn_open(
         JNIEnv* env, jobject /* obj */, jlong networkHandle, jstring apn, jint apnIpType) {
     if (apn == nullptr) {
@@ -1799,10 +1589,8 @@
         return;
     }
 
-    if (agnssIface_V2_0 != nullptr) {
-        AGnssDispatcher::dataConnOpen(agnssIface_V2_0, env, networkHandle, apn, apnIpType);
-    } else if (agnssIface != nullptr) {
-        AGnssDispatcher::dataConnOpen(agnssIface, env, apn, apnIpType);
+    if (agnssIface != nullptr) {
+        agnssIface->dataConnOpen(env, networkHandle, apn, apnIpType);
     } else {
         ALOGE("%s: IAGnss interface not available.", __func__);
         return;
@@ -1811,10 +1599,8 @@
 
 static void android_location_GnssNetworkConnectivityHandler_agps_data_conn_closed(JNIEnv* /* env */,
                                                                        jobject /* obj */) {
-    if (agnssIface_V2_0 != nullptr) {
-        AGnssDispatcher::dataConnClosed(agnssIface_V2_0);
-    } else if (agnssIface != nullptr) {
-        AGnssDispatcher::dataConnClosed(agnssIface);
+    if (agnssIface != nullptr) {
+        agnssIface->dataConnClosed();
     } else {
         ALOGE("%s: IAGnss interface not available.", __func__);
         return;
@@ -1823,10 +1609,8 @@
 
 static void android_location_GnssNetworkConnectivityHandler_agps_data_conn_failed(JNIEnv* /* env */,
                                                                        jobject /* obj */) {
-    if (agnssIface_V2_0 != nullptr) {
-        AGnssDispatcher::dataConnFailed(agnssIface_V2_0);
-    } else if (agnssIface != nullptr) {
-        AGnssDispatcher::dataConnFailed(agnssIface);
+    if (agnssIface != nullptr) {
+        agnssIface->dataConnFailed();
     } else {
         ALOGE("%s: IAGnss interface not available.", __func__);
         return;
@@ -1835,12 +1619,8 @@
 
 static void android_location_gnss_hal_GnssNative_set_agps_server(JNIEnv* env, jclass, jint type,
                                                                  jstring hostname, jint port) {
-    if (agnssIface_V2_0 != nullptr) {
-        AGnssDispatcher::setServer<IAGnss_V2_0, IAGnssCallback_V2_0>(agnssIface_V2_0, env, type,
-                hostname, port);
-    } else if (agnssIface != nullptr) {
-        AGnssDispatcher::setServer<IAGnss_V1_0, IAGnssCallback_V1_0>(agnssIface, env, type,
-                hostname, port);
+    if (agnssIface != nullptr) {
+        agnssIface->setServer(env, type, hostname, port);
     } else {
         ALOGE("%s: IAGnss interface not available.", __func__);
         return;
diff --git a/services/core/jni/gnss/AGnss.cpp b/services/core/jni/gnss/AGnss.cpp
new file mode 100644
index 0000000..00403d6
--- /dev/null
+++ b/services/core/jni/gnss/AGnss.cpp
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+// Define LOG_TAG before <log/log.h> to overwrite the default value.
+#define LOG_TAG "AGnssJni"
+
+#include "AGnss.h"
+
+#include "Utils.h"
+
+using android::hardware::gnss::IAGnss;
+using IAGnss_V1_0 = android::hardware::gnss::V1_0::IAGnss;
+using IAGnss_V2_0 = android::hardware::gnss::V2_0::IAGnss;
+using IAGnssCallback_V1_0 = android::hardware::gnss::V1_0::IAGnssCallback;
+using IAGnssCallback_V2_0 = android::hardware::gnss::V2_0::IAGnssCallback;
+using AGnssType = android::hardware::gnss::IAGnssCallback::AGnssType;
+
+namespace android::gnss {
+
+// Implementation of AGnss (AIDL HAL)
+
+AGnss::AGnss(const sp<IAGnss>& iAGnss) : mIAGnss(iAGnss) {
+    assert(mIAGnss != nullptr);
+}
+
+jboolean AGnss::setCallback(const std::unique_ptr<AGnssCallback>& callback) {
+    auto status = mIAGnss->setCallback(callback->getAidl());
+    return checkAidlStatus(status, "IAGnssAidl setCallback() failed.");
+}
+
+jboolean AGnss::dataConnOpen(JNIEnv* env, jlong networkHandle, jstring apn, jint apnIpType) {
+    ScopedJniString jniApn{env, apn};
+    auto status = mIAGnss->dataConnOpen(networkHandle, String16(jniApn.c_str()),
+                                        static_cast<IAGnss::ApnIpType>(apnIpType));
+    return checkAidlStatus(status,
+                           "IAGnssAidl dataConnOpen() failed. APN and its IP type not set.");
+}
+
+jboolean AGnss::dataConnClosed() {
+    auto status = mIAGnss->dataConnClosed();
+    return checkAidlStatus(status, "IAGnssAidl dataConnClosed() failed.");
+}
+
+jboolean AGnss::dataConnFailed() {
+    auto status = mIAGnss->dataConnFailed();
+    return checkAidlStatus(status, "IAGnssAidl dataConnFailed() failed.");
+}
+
+jboolean AGnss::setServer(JNIEnv* env, jint type, jstring hostname, jint port) {
+    ScopedJniString jniHostName{env, hostname};
+    auto status =
+            mIAGnss->setServer(static_cast<AGnssType>(type), String16(jniHostName.c_str()), port);
+    return checkAidlStatus(status, "IAGnssAidl setServer() failed. Host name and port not set.");
+}
+
+// Implementation of AGnss_V1_0
+
+AGnss_V1_0::AGnss_V1_0(const sp<IAGnss_V1_0>& iAGnss) : mIAGnss_V1_0(iAGnss) {
+    assert(mIAGnss_V1_0 != nullptr);
+}
+
+jboolean AGnss_V1_0::setCallback(const std::unique_ptr<AGnssCallback>& callback) {
+    auto result = mIAGnss_V1_0->setCallback(callback->getV1_0());
+    return checkHidlReturn(result, "IAGnss_V1_0 setCallback() failed.");
+}
+
+jboolean AGnss_V1_0::dataConnOpen(JNIEnv* env, jlong, jstring apn, jint apnIpType) {
+    ScopedJniString jniApn{env, apn};
+    auto result =
+            mIAGnss_V1_0->dataConnOpen(jniApn, static_cast<IAGnss_V1_0::ApnIpType>(apnIpType));
+    return checkHidlReturn(result,
+                           "IAGnss_V1_0 dataConnOpen() failed. APN and its IP type not set.");
+}
+
+jboolean AGnss_V1_0::dataConnClosed() {
+    auto result = mIAGnss_V1_0->dataConnClosed();
+    return checkHidlReturn(result, "IAGnss_V1_0 dataConnClosed() failed.");
+}
+
+jboolean AGnss_V1_0::dataConnFailed() {
+    auto result = mIAGnss_V1_0->dataConnFailed();
+    return checkHidlReturn(result, "IAGnss_V1_0 dataConnFailed() failed.");
+}
+
+jboolean AGnss_V1_0::setServer(JNIEnv* env, jint type, jstring hostname, jint port) {
+    ScopedJniString jniHostName{env, hostname};
+    auto result = mIAGnss_V1_0->setServer(static_cast<IAGnssCallback_V1_0::AGnssType>(type),
+                                          jniHostName, port);
+    return checkHidlReturn(result, "IAGnss_V1_0 setServer() failed. Host name and port not set.");
+}
+
+// Implementation of AGnss_V2_0
+
+AGnss_V2_0::AGnss_V2_0(const sp<IAGnss_V2_0>& iAGnss) : mIAGnss_V2_0(iAGnss) {
+    assert(mIAGnss_V2_0 != nullptr);
+}
+
+jboolean AGnss_V2_0::setCallback(const std::unique_ptr<AGnssCallback>& callback) {
+    auto result = mIAGnss_V2_0->setCallback(callback->getV2_0());
+    return checkHidlReturn(result, "IAGnss_V2_0 setCallback() failed.");
+}
+
+jboolean AGnss_V2_0::dataConnOpen(JNIEnv* env, jlong networkHandle, jstring apn, jint apnIpType) {
+    ScopedJniString jniApn{env, apn};
+    auto result = mIAGnss_V2_0->dataConnOpen(static_cast<uint64_t>(networkHandle), jniApn,
+                                             static_cast<IAGnss_V2_0::ApnIpType>(apnIpType));
+    return checkHidlReturn(result,
+                           "IAGnss_V2_0 dataConnOpen() failed. APN and its IP type not set.");
+}
+
+jboolean AGnss_V2_0::dataConnClosed() {
+    auto result = mIAGnss_V2_0->dataConnClosed();
+    return checkHidlReturn(result, "IAGnss_V2_0 dataConnClosed() failed.");
+}
+
+jboolean AGnss_V2_0::dataConnFailed() {
+    auto result = mIAGnss_V2_0->dataConnFailed();
+    return checkHidlReturn(result, "IAGnss_V2_0 dataConnFailed() failed.");
+}
+
+jboolean AGnss_V2_0::setServer(JNIEnv* env, jint type, jstring hostname, jint port) {
+    ScopedJniString jniHostName{env, hostname};
+    auto result = mIAGnss_V2_0->setServer(static_cast<IAGnssCallback_V2_0::AGnssType>(type),
+                                          jniHostName, port);
+    return checkHidlReturn(result, "IAGnss_V2_0 setServer() failed. Host name and port not set.");
+}
+
+} // namespace android::gnss
diff --git a/services/core/jni/gnss/AGnss.h b/services/core/jni/gnss/AGnss.h
new file mode 100644
index 0000000..2828b82
--- /dev/null
+++ b/services/core/jni/gnss/AGnss.h
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+#ifndef _ANDROID_SERVER_GNSS_AGNSS_H
+#define _ANDROID_SERVER_GNSS_AGNSS_H
+
+#pragma once
+
+#ifndef LOG_TAG
+#error LOG_TAG must be defined before including this file.
+#endif
+
+#include <android/hardware/gnss/BnAGnss.h>
+#include <log/log.h>
+
+#include "AGnssCallback.h"
+#include "jni.h"
+
+namespace android::gnss {
+
+class AGnssInterface {
+public:
+    virtual ~AGnssInterface() {}
+    virtual jboolean setCallback(const std::unique_ptr<AGnssCallback>& callback) = 0;
+    virtual jboolean dataConnOpen(JNIEnv* env, jlong networkHandle, jstring apn,
+                                  jint apnIpType) = 0;
+    virtual jboolean dataConnClosed() = 0;
+    virtual jboolean dataConnFailed() = 0;
+    virtual jboolean setServer(JNIEnv* env, jint type, jstring hostname, jint port) = 0;
+};
+
+class AGnss : public AGnssInterface {
+public:
+    AGnss(const sp<android::hardware::gnss::IAGnss>& iAGnss);
+    jboolean setCallback(const std::unique_ptr<AGnssCallback>& callback) override;
+    jboolean dataConnOpen(JNIEnv* env, jlong networkHandle, jstring apn, jint apnIpType) override;
+    jboolean dataConnClosed() override;
+    jboolean dataConnFailed() override;
+    jboolean setServer(JNIEnv* env, jint type, jstring hostname, jint port) override;
+
+private:
+    const sp<android::hardware::gnss::IAGnss> mIAGnss;
+};
+
+class AGnss_V1_0 : public AGnssInterface {
+public:
+    AGnss_V1_0(const sp<android::hardware::gnss::V1_0::IAGnss>& iAGnss);
+    jboolean setCallback(const std::unique_ptr<AGnssCallback>& callback) override;
+    jboolean dataConnOpen(JNIEnv* env, jlong, jstring apn, jint apnIpType) override;
+    jboolean dataConnClosed() override;
+    jboolean dataConnFailed() override;
+    jboolean setServer(JNIEnv* env, jint type, jstring hostname, jint port) override;
+
+private:
+    const sp<android::hardware::gnss::V1_0::IAGnss> mIAGnss_V1_0;
+};
+
+class AGnss_V2_0 : public AGnssInterface {
+public:
+    AGnss_V2_0(const sp<android::hardware::gnss::V2_0::IAGnss>& iAGnss);
+    jboolean setCallback(const std::unique_ptr<AGnssCallback>& callback) override;
+    jboolean dataConnOpen(JNIEnv* env, jlong networkHandle, jstring apn, jint apnIpType) override;
+    jboolean dataConnClosed() override;
+    jboolean dataConnFailed() override;
+    jboolean setServer(JNIEnv* env, jint type, jstring hostname, jint port) override;
+
+private:
+    const sp<android::hardware::gnss::V2_0::IAGnss> mIAGnss_V2_0;
+};
+
+} // namespace android::gnss
+
+#endif // _ANDROID_SERVER_GNSS_AGNSS_H
diff --git a/services/core/jni/gnss/AGnssCallback.cpp b/services/core/jni/gnss/AGnssCallback.cpp
new file mode 100644
index 0000000..466cdfa
--- /dev/null
+++ b/services/core/jni/gnss/AGnssCallback.cpp
@@ -0,0 +1,133 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "AGnssCbJni"
+
+#include "AGnssCallback.h"
+
+namespace android::gnss {
+
+using binder::Status;
+using hardware::Return;
+using hardware::Void;
+using IAGnssCallback_V1_0 = android::hardware::gnss::V1_0::IAGnssCallback;
+using IAGnssCallback_V2_0 = android::hardware::gnss::V2_0::IAGnssCallback;
+
+namespace {
+
+jmethodID method_reportAGpsStatus;
+
+}
+
+void AGnss_class_init_once(JNIEnv* env, jclass clazz) {
+    method_reportAGpsStatus = env->GetMethodID(clazz, "reportAGpsStatus", "(II[B)V");
+}
+
+Status AGnssCallbackAidl::agnssStatusCb(AGnssType type, AGnssStatusValue status) {
+    AGnssCallbackUtil::agnssStatusCbImpl(type, status);
+    return Status::ok();
+}
+
+Return<void> AGnssCallback_V1_0::agnssStatusIpV6Cb(
+        const IAGnssCallback_V1_0::AGnssStatusIpV6& agps_status) {
+    JNIEnv* env = getJniEnv();
+    jbyteArray byteArray = nullptr;
+
+    byteArray = env->NewByteArray(16);
+    if (byteArray != nullptr) {
+        env->SetByteArrayRegion(byteArray, 0, 16, (const jbyte*)(agps_status.ipV6Addr.data()));
+    } else {
+        ALOGE("Unable to allocate byte array for IPv6 address.");
+    }
+
+    IF_ALOGD() {
+        // log the IP for reference in case there is a bogus value pushed by HAL
+        char str[INET6_ADDRSTRLEN];
+        inet_ntop(AF_INET6, agps_status.ipV6Addr.data(), str, INET6_ADDRSTRLEN);
+        ALOGD("AGPS IP is v6: %s", str);
+    }
+
+    jsize byteArrayLength = byteArray != nullptr ? env->GetArrayLength(byteArray) : 0;
+    ALOGV("Passing AGPS IP addr: size %d", byteArrayLength);
+    env->CallVoidMethod(mCallbacksObj, method_reportAGpsStatus, agps_status.type,
+                        agps_status.status, byteArray);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+
+    if (byteArray) {
+        env->DeleteLocalRef(byteArray);
+    }
+
+    return Void();
+}
+
+Return<void> AGnssCallback_V1_0::agnssStatusIpV4Cb(
+        const IAGnssCallback_V1_0::AGnssStatusIpV4& agps_status) {
+    JNIEnv* env = getJniEnv();
+    jbyteArray byteArray = nullptr;
+
+    uint32_t ipAddr = agps_status.ipV4Addr;
+    byteArray = convertToIpV4(ipAddr);
+
+    IF_ALOGD() {
+        /*
+         * log the IP for reference in case there is a bogus value pushed by
+         * HAL.
+         */
+        char str[INET_ADDRSTRLEN];
+        inet_ntop(AF_INET, &ipAddr, str, INET_ADDRSTRLEN);
+        ALOGD("AGPS IP is v4: %s", str);
+    }
+
+    jsize byteArrayLength = byteArray != nullptr ? env->GetArrayLength(byteArray) : 0;
+    ALOGV("Passing AGPS IP addr: size %d", byteArrayLength);
+    env->CallVoidMethod(mCallbacksObj, method_reportAGpsStatus, agps_status.type,
+                        agps_status.status, byteArray);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+
+    if (byteArray) {
+        env->DeleteLocalRef(byteArray);
+    }
+    return Void();
+}
+
+jbyteArray AGnssCallback_V1_0::convertToIpV4(uint32_t ip) {
+    if (INADDR_NONE == ip) {
+        return nullptr;
+    }
+
+    JNIEnv* env = getJniEnv();
+    jbyteArray byteArray = env->NewByteArray(4);
+    if (byteArray == nullptr) {
+        ALOGE("Unable to allocate byte array for IPv4 address");
+        return nullptr;
+    }
+
+    jbyte ipv4[4];
+    ALOGV("Converting IPv4 address byte array (net_order) %x", ip);
+    memcpy(ipv4, &ip, sizeof(ipv4));
+    env->SetByteArrayRegion(byteArray, 0, 4, (const jbyte*)ipv4);
+    return byteArray;
+}
+
+Return<void> AGnssCallback_V2_0::agnssStatusCb(IAGnssCallback_V2_0::AGnssType type,
+                                               IAGnssCallback_V2_0::AGnssStatusValue status) {
+    AGnssCallbackUtil::agnssStatusCbImpl(type, status);
+    return Void();
+}
+
+} // namespace android::gnss
diff --git a/services/core/jni/gnss/AGnssCallback.h b/services/core/jni/gnss/AGnssCallback.h
new file mode 100644
index 0000000..e9bb471
--- /dev/null
+++ b/services/core/jni/gnss/AGnssCallback.h
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+#ifndef _ANDROID_SERVER_GNSS_AGNSSCALLBACK_H
+#define _ANDROID_SERVER_GNSS_AGNSSCALLBACK_H
+
+#pragma once
+
+#ifndef LOG_TAG
+#error LOG_TAG must be defined before including this file.
+#endif
+
+#include <android/hardware/gnss/1.0/IAGnss.h>
+#include <android/hardware/gnss/2.0/IAGnss.h>
+#include <android/hardware/gnss/BnAGnssCallback.h>
+#include <arpa/inet.h>
+#include <log/log.h>
+
+#include <vector>
+
+#include "Utils.h"
+#include "jni.h"
+
+namespace android::gnss {
+
+namespace {
+
+extern jmethodID method_reportAGpsStatus;
+
+}
+
+void AGnss_class_init_once(JNIEnv* env, jclass clazz);
+
+/*
+ * AGnssCallbackAidl class implements the callback methods required by the
+ * android::hardware::gnss::IAGnss interface.
+ */
+class AGnssCallbackAidl : public android::hardware::gnss::BnAGnssCallback {
+public:
+    binder::Status agnssStatusCb(AGnssType type, AGnssStatusValue status) override;
+};
+
+/*
+ * AGnssCallback_V1_0 implements callback methods required by the IAGnssCallback 1.0 interface.
+ */
+class AGnssCallback_V1_0 : public android::hardware::gnss::V1_0::IAGnssCallback {
+public:
+    // Methods from ::android::hardware::gps::V1_0::IAGnssCallback follow.
+    hardware::Return<void> agnssStatusIpV6Cb(
+            const android::hardware::gnss::V1_0::IAGnssCallback::AGnssStatusIpV6& agps_status)
+            override;
+
+    hardware::Return<void> agnssStatusIpV4Cb(
+            const android::hardware::gnss::V1_0::IAGnssCallback::AGnssStatusIpV4& agps_status)
+            override;
+
+private:
+    jbyteArray convertToIpV4(uint32_t ip);
+};
+
+/*
+ * AGnssCallback_V2_0 implements callback methods required by the IAGnssCallback 2.0 interface.
+ */
+class AGnssCallback_V2_0 : public android::hardware::gnss::V2_0::IAGnssCallback {
+public:
+    // Methods from ::android::hardware::gps::V2_0::IAGnssCallback follow.
+    hardware::Return<void> agnssStatusCb(
+            android::hardware::gnss::V2_0::IAGnssCallback::AGnssType type,
+            android::hardware::gnss::V2_0::IAGnssCallback::AGnssStatusValue status) override;
+};
+
+class AGnssCallback {
+public:
+    AGnssCallback() {}
+    sp<AGnssCallbackAidl> getAidl() {
+        if (callbackAidl == nullptr) {
+            callbackAidl = sp<AGnssCallbackAidl>::make();
+        }
+        return callbackAidl;
+    }
+
+    sp<AGnssCallback_V1_0> getV1_0() {
+        if (callbackV1_0 == nullptr) {
+            callbackV1_0 = sp<AGnssCallback_V1_0>::make();
+        }
+        return callbackV1_0;
+    }
+
+    sp<AGnssCallback_V2_0> getV2_0() {
+        if (callbackV2_0 == nullptr) {
+            callbackV2_0 = sp<AGnssCallback_V2_0>::make();
+        }
+        return callbackV2_0;
+    }
+
+private:
+    sp<AGnssCallbackAidl> callbackAidl;
+    sp<AGnssCallback_V1_0> callbackV1_0;
+    sp<AGnssCallback_V2_0> callbackV2_0;
+};
+
+struct AGnssCallbackUtil {
+    template <class T, class U>
+    static void agnssStatusCbImpl(const T& type, const U& status);
+
+private:
+    AGnssCallbackUtil() = delete;
+};
+
+template <class T, class U>
+void AGnssCallbackUtil::agnssStatusCbImpl(const T& type, const U& status) {
+    ALOGD("%s. type: %d, status:%d", __func__, static_cast<int32_t>(type),
+          static_cast<int32_t>(status));
+    JNIEnv* env = getJniEnv();
+    env->CallVoidMethod(mCallbacksObj, method_reportAGpsStatus, type, status, nullptr);
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+}
+
+} // namespace android::gnss
+
+#endif // _ANDROID_SERVER_GNSS_AGNSSCALLBACK_H
\ No newline at end of file
diff --git a/services/core/jni/gnss/Android.bp b/services/core/jni/gnss/Android.bp
index ac50bfa..e49e81c 100644
--- a/services/core/jni/gnss/Android.bp
+++ b/services/core/jni/gnss/Android.bp
@@ -23,6 +23,8 @@
     ],
 
     srcs: [
+        "AGnss.cpp",
+        "AGnssCallback.cpp",
         "GnssAntennaInfoCallback.cpp",
         "GnssBatching.cpp",
         "GnssBatchingCallback.cpp",
diff --git a/services/core/jni/gnss/Utils.h b/services/core/jni/gnss/Utils.h
index 1bd69c4..2640a77 100644
--- a/services/core/jni/gnss/Utils.h
+++ b/services/core/jni/gnss/Utils.h
@@ -196,6 +196,30 @@
     JNIEnv* mEnv = nullptr;
 };
 
+struct ScopedJniString {
+    ScopedJniString(JNIEnv* env, jstring javaString) : mEnv(env), mJavaString(javaString) {
+        mNativeString = mEnv->GetStringUTFChars(mJavaString, nullptr);
+    }
+
+    ~ScopedJniString() {
+        if (mNativeString != nullptr) {
+            mEnv->ReleaseStringUTFChars(mJavaString, mNativeString);
+        }
+    }
+
+    const char* c_str() const { return mNativeString; }
+
+    operator hardware::hidl_string() const { return hardware::hidl_string(mNativeString); }
+
+private:
+    ScopedJniString(const ScopedJniString&) = delete;
+    ScopedJniString& operator=(const ScopedJniString&) = delete;
+
+    JNIEnv* mEnv;
+    jstring mJavaString;
+    const char* mNativeString;
+};
+
 JNIEnv* getJniEnv();
 
 template <class T>
diff --git a/services/robotests/backup/src/com/android/server/backup/internal/PerformInitializeTaskTest.java b/services/robotests/backup/src/com/android/server/backup/internal/PerformInitializeTaskTest.java
index bf4eeae..8561651 100644
--- a/services/robotests/backup/src/com/android/server/backup/internal/PerformInitializeTaskTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/internal/PerformInitializeTaskTest.java
@@ -43,13 +43,13 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.Log;
 
-import com.android.internal.backup.IBackupTransport;
 import com.android.server.backup.BackupManagerService;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.testing.TransportData;
 import com.android.server.backup.testing.TransportTestUtils;
 import com.android.server.backup.testing.TransportTestUtils.TransportMock;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.testing.shadows.ShadowSlog;
 
@@ -75,7 +75,7 @@
     @Mock private UserBackupManagerService mBackupManagerService;
     @Mock private TransportManager mTransportManager;
     @Mock private OnTaskFinishedListener mListener;
-    @Mock private IBackupTransport mTransportBinder;
+    @Mock private BackupTransportClient mTransportClient;
     @Mock private IBackupObserver mObserver;
     @Mock private AlarmManager mAlarmManager;
     @Mock private PendingIntent mRunInitIntent;
@@ -101,19 +101,19 @@
     @Test
     public void testRun_callsTransportCorrectly() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+        configureTransport(mTransportClient, TRANSPORT_OK, TRANSPORT_OK);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
 
-        verify(mTransportBinder).initializeDevice();
-        verify(mTransportBinder).finishBackup();
+        verify(mTransportClient).initializeDevice();
+        verify(mTransportClient).finishBackup();
     }
 
     @Test
     public void testRun_callsBackupManagerCorrectly() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+        configureTransport(mTransportClient, TRANSPORT_OK, TRANSPORT_OK);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
@@ -127,7 +127,7 @@
     @Test
     public void testRun_callsObserverAndListenerCorrectly() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+        configureTransport(mTransportClient, TRANSPORT_OK, TRANSPORT_OK);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
@@ -140,13 +140,13 @@
     @Test
     public void testRun_whenInitializeDeviceFails() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+        configureTransport(mTransportClient, TRANSPORT_ERROR, 0);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
 
-        verify(mTransportBinder).initializeDevice();
-        verify(mTransportBinder, never()).finishBackup();
+        verify(mTransportClient).initializeDevice();
+        verify(mTransportClient, never()).finishBackup();
         verify(mBackupManagerService)
                 .recordInitPending(true, mTransportName, mTransport.transportDirName);
     }
@@ -155,7 +155,7 @@
     public void testRun_whenInitializeDeviceFails_callsObserverAndListenerCorrectly()
             throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+        configureTransport(mTransportClient, TRANSPORT_ERROR, 0);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
@@ -168,7 +168,7 @@
     @Test
     public void testRun_whenInitializeDeviceFails_schedulesAlarm() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+        configureTransport(mTransportClient, TRANSPORT_ERROR, 0);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
@@ -179,13 +179,13 @@
     @Test
     public void testRun_whenFinishBackupFails() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+        configureTransport(mTransportClient, TRANSPORT_OK, TRANSPORT_ERROR);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
 
-        verify(mTransportBinder).initializeDevice();
-        verify(mTransportBinder).finishBackup();
+        verify(mTransportClient).initializeDevice();
+        verify(mTransportClient).finishBackup();
         verify(mBackupManagerService)
                 .recordInitPending(true, mTransportName, mTransport.transportDirName);
     }
@@ -193,7 +193,7 @@
     @Test
     public void testRun_whenFinishBackupFails_callsObserverAndListenerCorrectly() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+        configureTransport(mTransportClient, TRANSPORT_OK, TRANSPORT_ERROR);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
@@ -206,7 +206,7 @@
     @Test
     public void testRun_whenFinishBackupFails_logs() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+        configureTransport(mTransportClient, TRANSPORT_OK, TRANSPORT_ERROR);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
@@ -219,7 +219,7 @@
     @Test
     public void testRun_whenInitializeDeviceFails_logs() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+        configureTransport(mTransportClient, TRANSPORT_ERROR, 0);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
@@ -232,7 +232,7 @@
     @Test
     public void testRun_whenFinishBackupFails_schedulesAlarm() throws Exception {
         setUpTransport(mTransport);
-        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+        configureTransport(mTransportClient, TRANSPORT_OK, TRANSPORT_ERROR);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
 
         performInitializeTask.run();
@@ -327,7 +327,7 @@
         List<TransportMock> transportMocks =
                 setUpTransports(mTransportManager, transport1, transport2);
         String registeredTransportName = transport2.transportName;
-        IBackupTransport registeredTransport = transportMocks.get(1).transport;
+        BackupTransportClient registeredTransport = transportMocks.get(1).transport;
         TransportConnection
                 registeredTransportConnection = transportMocks.get(1).mTransportConnection;
         PerformInitializeTask performInitializeTask =
@@ -357,7 +357,7 @@
     @Test
     public void testRun_whenTransportThrowsDeadObjectException() throws Exception {
         TransportMock transportMock = setUpTransport(mTransport);
-        IBackupTransport transport = transportMock.transport;
+        BackupTransportClient transport = transportMock.transport;
         TransportConnection transportConnection = transportMock.mTransportConnection;
         when(transport.initializeDevice()).thenThrow(DeadObjectException.class);
         PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
@@ -380,7 +380,7 @@
     }
 
     private void configureTransport(
-            IBackupTransport transportMock, int initializeDeviceStatus, int finishBackupStatus)
+            BackupTransportClient transportMock, int initializeDeviceStatus, int finishBackupStatus)
             throws Exception {
         when(transportMock.initializeDevice()).thenReturn(initializeDeviceStatus);
         when(transportMock.finishBackup()).thenReturn(finishBackupStatus);
@@ -389,7 +389,7 @@
     private TransportMock setUpTransport(TransportData transport) throws Exception {
         TransportMock transportMock =
                 TransportTestUtils.setUpTransport(mTransportManager, transport);
-        mTransportBinder = transportMock.transport;
+        mTransportClient = transportMock.transport;
         return transportMock;
     }
 }
diff --git a/services/robotests/backup/src/com/android/server/backup/testing/TransportTestUtils.java b/services/robotests/backup/src/com/android/server/backup/testing/TransportTestUtils.java
index ce44f06..8131ac4 100644
--- a/services/robotests/backup/src/com/android/server/backup/testing/TransportTestUtils.java
+++ b/services/robotests/backup/src/com/android/server/backup/testing/TransportTestUtils.java
@@ -34,8 +34,8 @@
 import android.content.pm.ServiceInfo;
 import android.os.RemoteException;
 
-import com.android.internal.backup.IBackupTransport;
 import com.android.server.backup.TransportManager;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.transport.TransportNotAvailableException;
 import com.android.server.backup.transport.TransportNotRegisteredException;
@@ -160,7 +160,7 @@
             when(transportConnectionMock.getTransportComponent()).thenReturn(transportComponent);
             if (status == TransportStatus.REGISTERED_AVAILABLE) {
                 // Transport registered and available
-                IBackupTransport transportMock = mockTransportBinder(transport);
+                BackupTransportClient transportMock = mockTransportBinder(transport);
                 when(transportConnectionMock.connectOrThrow(any())).thenReturn(transportMock);
                 when(transportConnectionMock.connect(any())).thenReturn(transportMock);
 
@@ -179,8 +179,9 @@
         }
     }
 
-    private static IBackupTransport mockTransportBinder(TransportData transport) throws Exception {
-        IBackupTransport transportBinder = mock(IBackupTransport.class);
+    private static BackupTransportClient mockTransportBinder(TransportData transport)
+            throws Exception {
+        BackupTransportClient transportBinder = mock(BackupTransportClient.class);
         try {
             when(transportBinder.name()).thenReturn(transport.transportName);
             when(transportBinder.transportDirName()).thenReturn(transport.transportDirName);
@@ -199,12 +200,12 @@
     public static class TransportMock {
         public final TransportData transportData;
         @Nullable public final TransportConnection mTransportConnection;
-        @Nullable public final IBackupTransport transport;
+        @Nullable public final BackupTransportClient transport;
 
         private TransportMock(
                 TransportData transportData,
                 @Nullable TransportConnection transportConnection,
-                @Nullable IBackupTransport transport) {
+                @Nullable BackupTransportClient transport) {
             this.transportData = transportData;
             this.mTransportConnection = transportConnection;
             this.transport = transport;
diff --git a/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java b/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java
index de4aec6..6a82f16 100644
--- a/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java
@@ -84,9 +84,11 @@
     @Mock private TransportConnectionListener mTransportConnectionListener;
     @Mock private TransportConnectionListener mTransportConnectionListener2;
     @Mock private IBackupTransport.Stub mTransportBinder;
+
     @UserIdInt private int mUserId;
     private TransportStats mTransportStats;
     private TransportConnection mTransportConnection;
+    private BackupTransportClient mTransportClient;
     private ComponentName mTransportComponent;
     private String mTransportString;
     private Intent mBindIntent;
@@ -116,6 +118,7 @@
                         "1",
                         "caller",
                         new Handler(mainLooper));
+        mTransportClient = new BackupTransportClient(mTransportBinder);
 
         when(mContext.bindServiceAsUser(
                         eq(mBindIntent),
@@ -156,7 +159,8 @@
 
         mShadowMainLooper.runToEndOfTasks();
         verify(mTransportConnectionListener)
-                .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportConnection));
+                .onTransportConnectionResult(any(BackupTransportClient.class),
+                        eq(mTransportConnection));
     }
 
     @Test
@@ -169,9 +173,11 @@
         connection.onServiceConnected(mTransportComponent, mTransportBinder);
         mShadowMainLooper.runToEndOfTasks();
         verify(mTransportConnectionListener)
-                .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportConnection));
+                .onTransportConnectionResult(any(BackupTransportClient.class),
+                        eq(mTransportConnection));
         verify(mTransportConnectionListener2)
-                .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportConnection));
+                .onTransportConnectionResult(any(BackupTransportClient.class),
+                        eq(mTransportConnection));
     }
 
     @Test
@@ -184,7 +190,8 @@
 
         mShadowMainLooper.runToEndOfTasks();
         verify(mTransportConnectionListener2)
-                .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportConnection));
+                .onTransportConnectionResult(any(BackupTransportClient.class),
+                        eq(mTransportConnection));
     }
 
     @Test
@@ -312,10 +319,10 @@
         ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
         connection.onServiceConnected(mTransportComponent, mTransportBinder);
 
-        IBackupTransport transportBinder =
+        BackupTransportClient transportClient =
                 runInWorkerThread(() -> mTransportConnection.connect("caller2"));
 
-        assertThat(transportBinder).isNotNull();
+        assertThat(transportClient).isNotNull();
     }
 
     @Test
@@ -325,10 +332,10 @@
         connection.onServiceConnected(mTransportComponent, mTransportBinder);
         connection.onServiceDisconnected(mTransportComponent);
 
-        IBackupTransport transportBinder =
+        BackupTransportClient transportClient =
                 runInWorkerThread(() -> mTransportConnection.connect("caller2"));
 
-        assertThat(transportBinder).isNull();
+        assertThat(transportClient).isNull();
     }
 
     @Test
@@ -337,10 +344,10 @@
         ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
         connection.onBindingDied(mTransportComponent);
 
-        IBackupTransport transportBinder =
+        BackupTransportClient transportClient =
                 runInWorkerThread(() -> mTransportConnection.connect("caller2"));
 
-        assertThat(transportBinder).isNull();
+        assertThat(transportClient).isNull();
     }
 
     @Test
@@ -354,17 +361,17 @@
         doAnswer(
                         invocation -> {
                             TransportConnectionListener listener = invocation.getArgument(0);
-                            listener.onTransportConnectionResult(mTransportBinder,
+                            listener.onTransportConnectionResult(mTransportClient,
                                     transportConnection);
                             return null;
                         })
                 .when(transportConnection)
                 .connectAsync(any(), any());
 
-        IBackupTransport transportBinder =
+        BackupTransportClient transportClient =
                 runInWorkerThread(() -> transportConnection.connect("caller"));
 
-        assertThat(transportBinder).isNotNull();
+        assertThat(transportClient).isNotNull();
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
index aa7d6aa..c36e1a8 100644
--- a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -31,13 +31,13 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
-import com.android.internal.backup.IBackupTransport;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.backup.internal.OnTaskFinishedListener;
 import com.android.server.backup.params.BackupParams;
+import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.utils.BackupEligibilityRules;
 
@@ -57,9 +57,8 @@
     @Mock IBackupManagerMonitor mBackupManagerMonitor;
     @Mock IBackupObserver mBackupObserver;
     @Mock PackageManager mPackageManager;
-    @Mock
-    TransportConnection mTransportConnection;
-    @Mock IBackupTransport mBackupTransport;
+    @Mock TransportConnection mTransportConnection;
+    @Mock BackupTransportClient mBackupTransport;
     @Mock BackupEligibilityRules mBackupEligibilityRules;
 
 
diff --git a/services/tests/servicestests/src/com/android/server/backup/internal/LifecycleOperationStorageTest.java b/services/tests/servicestests/src/com/android/server/backup/internal/LifecycleOperationStorageTest.java
new file mode 100644
index 0000000..948accc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/backup/internal/LifecycleOperationStorageTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.backup.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.OperationStorage.OpState;
+import com.android.server.backup.OperationStorage.OpType;
+
+import com.google.android.collect.Sets;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Set;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LifecycleOperationStorageTest {
+    private static final int USER_ID = 0;
+    private static final int TOKEN_1 = 1;
+    private static final int TOKEN_2 = 2;
+    private static final int TOKEN_3 = 3;
+    private static final long RESULT = 123L;
+
+    private static final String PKG_FOO = "com.android.foo";
+    private static final String PKG_BAR = "com.android.bar";
+    private static final String PKG_BAZ = "com.android.baz";
+    private static final Set<String> MULTIPLE_PKG    = Sets.newHashSet(PKG_FOO);
+    private static final Set<String> MULTIPLE_PKGS_1 = Sets.newHashSet(PKG_FOO, PKG_BAR);
+    private static final Set<String> MULTIPLE_PKGS_2 = Sets.newHashSet(PKG_BAR, PKG_BAZ);
+
+    @Mock private BackupRestoreTask mCallback;
+    private LifecycleOperationStorage mOpStorage;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(/* testClass */ this);
+        mOpStorage = new LifecycleOperationStorage(USER_ID);
+    }
+
+    @After
+    public void tearDown() {}
+
+    @Test
+    public void testRegisterOperation_singleOperation() throws Exception {
+        mOpStorage.registerOperation(TOKEN_1, OpState.PENDING, mCallback, OpType.BACKUP_WAIT);
+
+        Set<Integer> tokens = mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT);
+
+        assertThat(mOpStorage.numOperations()).isEqualTo(1);
+        assertThat(tokens).isEqualTo(only(TOKEN_1));
+    }
+
+    @Test
+    public void testRegisterOperation_multipleOperations() throws Exception {
+        mOpStorage.registerOperation(TOKEN_1, OpState.PENDING, mCallback, OpType.BACKUP_WAIT);
+        mOpStorage.registerOperation(TOKEN_2, OpState.ACKNOWLEDGED, mCallback, OpType.BACKUP_WAIT);
+
+        Set<Integer> typeWaitTokens = mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT);
+        Set<Integer> statePendingTokens = mOpStorage.operationTokensForOpState(OpState.PENDING);
+        Set<Integer> stateAcknowledgedTokens =
+                mOpStorage.operationTokensForOpState(OpState.ACKNOWLEDGED);
+
+        assertThat(mOpStorage.numOperations()).isEqualTo(2);
+        assertThat(typeWaitTokens).isEqualTo(Sets.newHashSet(TOKEN_1, TOKEN_2));
+        assertThat(statePendingTokens).isEqualTo(only(TOKEN_1));
+        assertThat(stateAcknowledgedTokens).isEqualTo(only(TOKEN_2));
+    }
+
+    @Test
+    public void testRegisterOperationForPackages_singlePackage() throws Exception {
+        mOpStorage.registerOperationForPackages(TOKEN_1, OpState.PENDING,
+                MULTIPLE_PKG, mCallback, OpType.BACKUP_WAIT);
+
+        Set<Integer> tokens = mOpStorage.operationTokensForPackage(PKG_FOO);
+
+        assertThat(mOpStorage.numOperations()).isEqualTo(1);
+        assertThat(tokens).isEqualTo(only(TOKEN_1));
+    }
+
+    @Test
+    public void testRegisterOperationForPackages_multiplePackage() throws Exception {
+        mOpStorage.registerOperationForPackages(TOKEN_1, OpState.PENDING,
+                MULTIPLE_PKGS_1, mCallback, OpType.BACKUP);
+        mOpStorage.registerOperationForPackages(TOKEN_2, OpState.PENDING,
+                MULTIPLE_PKGS_2, mCallback, OpType.BACKUP);
+
+        Set<Integer> tokensFoo = mOpStorage.operationTokensForPackage(PKG_FOO);
+        Set<Integer> tokensBar = mOpStorage.operationTokensForPackage(PKG_BAR);
+        Set<Integer> tokensBaz = mOpStorage.operationTokensForPackage(PKG_BAZ);
+
+        assertThat(mOpStorage.numOperations()).isEqualTo(2);
+        assertThat(tokensFoo).isEqualTo(only(TOKEN_1));
+        assertThat(tokensBar).isEqualTo(Sets.newHashSet(TOKEN_1, TOKEN_2));
+        assertThat(tokensBaz).isEqualTo(only(TOKEN_2));
+    }
+
+    @Test
+    public void testRemoveOperation() throws Exception {
+        mOpStorage.registerOperation(TOKEN_2, OpState.PENDING, mCallback, OpType.BACKUP_WAIT);
+
+        Set<Integer> typeWaitTokens = mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT);
+        Set<Integer> statePendingTokens = mOpStorage.operationTokensForOpState(OpState.PENDING);
+
+        assertThat(mOpStorage.numOperations()).isEqualTo(1);
+        assertThat(typeWaitTokens).isEqualTo(only(TOKEN_2));
+        assertThat(statePendingTokens).isEqualTo(only(TOKEN_2));
+
+        mOpStorage.removeOperation(TOKEN_2);
+
+        typeWaitTokens = mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT);
+        statePendingTokens = mOpStorage.operationTokensForOpState(OpState.PENDING);
+
+        assertThat(mOpStorage.numOperations()).isEqualTo(0);
+        assertThat(typeWaitTokens).isEmpty();
+        assertThat(statePendingTokens).isEmpty();
+    }
+
+    @Test
+    public void testRemoveOperation_removesPackageMappings() throws Exception {
+        mOpStorage.registerOperationForPackages(TOKEN_1, OpState.PENDING, MULTIPLE_PKGS_1,
+                mCallback, OpType.BACKUP);
+        mOpStorage.registerOperationForPackages(TOKEN_2, OpState.PENDING, MULTIPLE_PKGS_2,
+                mCallback, OpType.BACKUP);
+
+        mOpStorage.removeOperation(TOKEN_2);
+
+        Set<Integer> tokensFoo = mOpStorage.operationTokensForPackage(PKG_FOO);
+        Set<Integer> tokensBar = mOpStorage.operationTokensForPackage(PKG_BAR);
+        Set<Integer> tokensBaz = mOpStorage.operationTokensForPackage(PKG_BAZ);
+
+        assertThat(mOpStorage.numOperations()).isEqualTo(1);
+        assertThat(tokensFoo).isEqualTo(only(TOKEN_1));
+        assertThat(tokensBar).isEqualTo(only(TOKEN_1));
+        assertThat(tokensBaz).isEmpty();
+    }
+
+    @Test
+    public void testIsBackupOperationInProgress() throws Exception {
+        mOpStorage.registerOperation(TOKEN_1, OpState.ACKNOWLEDGED, mCallback, OpType.RESTORE_WAIT);
+        assertThat(mOpStorage.isBackupOperationInProgress()).isFalse();
+
+        mOpStorage.registerOperation(TOKEN_2, OpState.TIMEOUT, mCallback, OpType.BACKUP_WAIT);
+        assertThat(mOpStorage.isBackupOperationInProgress()).isFalse();
+
+        mOpStorage.registerOperation(TOKEN_3, OpState.PENDING, mCallback, OpType.BACKUP);
+        assertThat(mOpStorage.isBackupOperationInProgress()).isTrue();
+    }
+
+    @Test
+    public void testOnOperationComplete_pendingAdvancesState_invokesCallback() throws Exception {
+        mOpStorage.registerOperation(TOKEN_1, OpState.PENDING, mCallback, OpType.BACKUP_WAIT);
+
+        mOpStorage.onOperationComplete(TOKEN_1, RESULT, callback -> {
+            mCallback.operationComplete(RESULT);
+        });
+
+        assertThat(mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT))
+                .isEqualTo(only(TOKEN_1));
+        assertThat(mOpStorage.operationTokensForOpState(OpState.PENDING)).isEmpty();
+        assertThat(mOpStorage.operationTokensForOpState(OpState.ACKNOWLEDGED)).isNotEmpty();
+        verify(mCallback).operationComplete(RESULT);
+    }
+
+    private Set<Integer> only(Integer val) {
+        return Sets.newHashSet(val);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index b3f7587..b255a35 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -302,6 +302,65 @@
         testInvokesCancel(session -> session.onDialogDismissed(DISMISSED_REASON_NEGATIVE, null));
     }
 
+    // TODO (b/208484275) : Enable these tests
+    // @Test
+    // public void testPreAuth_canAuthAndPrivacyDisabled() throws Exception {
+    //     SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class);
+    //     when(manager
+    //             .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt()))
+    //             .thenReturn(false);
+    //     when(mContext.getSystemService(SensorPrivacyManager.class))
+    //             .thenReturn(manager);
+    //     setupFace(1 /* id */, false /* confirmationAlwaysRequired */,
+    //             mock(IBiometricAuthenticator.class));
+    //     final PromptInfo promptInfo = createPromptInfo(Authenticators.BIOMETRIC_STRONG);
+    //     final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false);
+    //     assertEquals(BiometricManager.BIOMETRIC_SUCCESS, preAuthInfo.getCanAuthenticateResult());
+    //     for (BiometricSensor sensor : preAuthInfo.eligibleSensors) {
+    //         assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState());
+    //     }
+    // }
+
+    // @Test
+    // public void testPreAuth_cannotAuthAndPrivacyEnabled() throws Exception {
+    //     SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class);
+    //     when(manager
+    //             .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt()))
+    //             .thenReturn(true);
+    //     when(mContext.getSystemService(SensorPrivacyManager.class))
+    //             .thenReturn(manager);
+    //     setupFace(1 /* id */, false /* confirmationAlwaysRequired */,
+    //             mock(IBiometricAuthenticator.class));
+    //     final PromptInfo promptInfo = createPromptInfo(Authenticators.BIOMETRIC_STRONG);
+    //     final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false);
+    //     assertEquals(BiometricManager.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED,
+    //             preAuthInfo.getCanAuthenticateResult());
+    //     // Even though canAuth returns privacy enabled, we should still be able to authenticate.
+    //     for (BiometricSensor sensor : preAuthInfo.eligibleSensors) {
+    //         assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState());
+    //     }
+    // }
+
+    // @Test
+    // public void testPreAuth_canAuthAndPrivacyEnabledCredentialEnabled() throws Exception {
+    //     SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class);
+    //     when(manager
+    //             .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt()))
+    //             .thenReturn(true);
+    //     when(mContext.getSystemService(SensorPrivacyManager.class))
+    //             .thenReturn(manager);
+    //     setupFace(1 /* id */, false /* confirmationAlwaysRequired */,
+    //             mock(IBiometricAuthenticator.class));
+    //     final PromptInfo promptInfo =
+    //             createPromptInfo(Authenticators.BIOMETRIC_STRONG
+    //             | Authenticators. DEVICE_CREDENTIAL);
+    //     final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false);
+    //     assertEquals(BiometricManager.BIOMETRIC_SUCCESS, preAuthInfo.getCanAuthenticateResult());
+    //     for (BiometricSensor sensor : preAuthInfo.eligibleSensors) {
+    //         assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState());
+    //     }
+    // }
+
     private void testInvokesCancel(Consumer<AuthSession> sessionConsumer) throws RemoteException {
         final IBiometricAuthenticator faceAuthenticator = mock(IBiometricAuthenticator.class);
 
@@ -331,7 +390,8 @@
                 userId,
                 promptInfo,
                 TEST_PACKAGE,
-                checkDevicePolicyManager);
+                checkDevicePolicyManager,
+                mContext);
     }
 
     private AuthSession createAuthSession(List<BiometricSensor> sensors,
diff --git a/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java b/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java
new file mode 100644
index 0000000..f172279
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java
@@ -0,0 +1,130 @@
+/*
+ * 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.statusbar;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.testing.TestableContext;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+
+/**
+ * {@link ContextWrapper} that doesn't register {@link BroadcastReceiver}.
+ *
+ * Instead, it keeps a list of the registrations for querying.
+ */
+class NoBroadcastContextWrapper extends TestableContext {
+
+    ArrayList<BroadcastReceiverRegistration> mRegistrationList =
+            new ArrayList<>();
+
+    NoBroadcastContextWrapper(Context context) {
+        super(context);
+    }
+
+    @Override
+    public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter) {
+        return registerReceiver(receiver, filter, 0);
+    }
+
+    @Override
+    public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter,
+            int flags) {
+        return registerReceiver(receiver, filter, null, null, flags);
+    }
+
+    @Override
+    public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter,
+            @Nullable String broadcastPermission, @Nullable Handler scheduler) {
+        return registerReceiver(receiver, filter, broadcastPermission, scheduler, 0);
+    }
+
+    @Override
+    public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter,
+            @Nullable String broadcastPermission, @Nullable Handler scheduler, int flags) {
+        return registerReceiverAsUser(receiver, getUser(), filter, broadcastPermission, scheduler,
+                flags);
+    }
+
+    @Nullable
+    @Override
+    public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver,
+            @NonNull IntentFilter filter, @Nullable String broadcastPermission,
+            @Nullable Handler scheduler) {
+        return registerReceiverForAllUsers(receiver, filter, broadcastPermission, scheduler, 0);
+    }
+
+    @Nullable
+    @Override
+    public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver,
+            @NonNull IntentFilter filter, @Nullable String broadcastPermission,
+            @Nullable Handler scheduler, int flags) {
+        return registerReceiverAsUser(receiver, UserHandle.ALL, filter, broadcastPermission,
+                scheduler, flags);
+    }
+
+    @Override
+    public Intent registerReceiverAsUser(@Nullable BroadcastReceiver receiver, UserHandle user,
+            IntentFilter filter, @Nullable String broadcastPermission,
+            @Nullable Handler scheduler) {
+        return registerReceiverAsUser(receiver, user, filter, broadcastPermission,
+                scheduler, 0);
+    }
+
+    @Override
+    public Intent registerReceiverAsUser(@Nullable BroadcastReceiver receiver, UserHandle user,
+            IntentFilter filter, @Nullable String broadcastPermission,
+            @Nullable Handler scheduler, int flags) {
+        BroadcastReceiverRegistration reg = new BroadcastReceiverRegistration(
+                receiver, user, filter, broadcastPermission, scheduler, flags
+        );
+        mRegistrationList.add(reg);
+        return null;
+    }
+
+    @Override
+    public void unregisterReceiver(BroadcastReceiver receiver) {
+        mRegistrationList.removeIf((reg) -> reg.mReceiver == receiver);
+    }
+
+    static class BroadcastReceiverRegistration {
+        final BroadcastReceiver mReceiver;
+        final UserHandle mUser;
+        final IntentFilter mIntentFilter;
+        final String mBroadcastPermission;
+        final Handler mHandler;
+        final int mFlags;
+
+        BroadcastReceiverRegistration(BroadcastReceiver receiver, UserHandle user,
+                IntentFilter intentFilter, String broadcastPermission, Handler handler, int flags) {
+            mReceiver = receiver;
+            mUser = user;
+            mIntentFilter = intentFilter;
+            mBroadcastPermission = broadcastPermission;
+            mHandler = handler;
+            mFlags = flags;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
new file mode 100644
index 0000000..c293b5e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
@@ -0,0 +1,659 @@
+/*
+ * 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.statusbar;
+
+import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.app.ActivityManagerInternal;
+import android.app.StatusBarManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.graphics.drawable.Icon;
+import android.hardware.display.DisplayManager;
+import android.os.Binder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.quicksettings.TileService;
+import android.testing.TestableContext;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.statusbar.IAddTileResultCallback;
+import com.android.internal.statusbar.IStatusBar;
+import com.android.server.LocalServices;
+import com.android.server.policy.GlobalActionsProvider;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class StatusBarManagerServiceTest {
+
+    private static final String TEST_PACKAGE = "test_pkg";
+    private static final String TEST_SERVICE = "test_svc";
+    private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PACKAGE,
+            TEST_SERVICE);
+    private static final CharSequence APP_NAME = "AppName";
+    private static final CharSequence TILE_LABEL = "Tile label";
+
+    @Rule
+    public final TestableContext mContext =
+            new NoBroadcastContextWrapper(InstrumentationRegistry.getContext());
+
+    @Mock
+    private ActivityTaskManagerInternal mActivityTaskManagerInternal;
+    @Mock
+    private PackageManagerInternal mPackageManagerInternal;
+    @Mock
+    private ActivityManagerInternal mActivityManagerInternal;
+    @Mock
+    private ApplicationInfo mApplicationInfo;
+    @Mock
+    private IStatusBar.Stub mMockStatusBar;
+    @Captor
+    private ArgumentCaptor<IAddTileResultCallback> mAddTileResultCallbackCaptor;
+
+    private Icon mIcon;
+    private StatusBarManagerService mStatusBarManagerService;
+
+    @BeforeClass
+    public static void oneTimeInitialization() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
+        LocalServices.addService(ActivityTaskManagerInternal.class, mActivityTaskManagerInternal);
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+        LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInternal);
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal);
+
+        when(mMockStatusBar.asBinder()).thenReturn(mMockStatusBar);
+        when(mApplicationInfo.loadLabel(any())).thenReturn(APP_NAME);
+
+        mStatusBarManagerService = new StatusBarManagerService(mContext);
+        LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
+        LocalServices.removeServiceForTest(GlobalActionsProvider.class);
+
+        mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(
+                mStatusBarManagerService);
+
+        mStatusBarManagerService.registerStatusBar(mMockStatusBar);
+
+        mIcon = Icon.createWithResource(mContext, android.R.drawable.btn_plus);
+    }
+
+    @Test
+    public void testHandleIncomingUserCalled() {
+        int fakeUser = 17;
+        try {
+            mStatusBarManagerService.requestAddTile(
+                    TEST_COMPONENT,
+                    TILE_LABEL,
+                    mIcon,
+                    fakeUser,
+                    new Callback()
+            );
+            fail("Should have SecurityException from uid check");
+        } catch (SecurityException e) {
+            verify(mActivityManagerInternal).handleIncomingUser(
+                    eq(Binder.getCallingPid()),
+                    eq(Binder.getCallingUid()),
+                    eq(fakeUser),
+                    eq(false),
+                    eq(ActivityManagerInternal.ALLOW_NON_FULL),
+                    anyString(),
+                    eq(TEST_PACKAGE)
+            );
+        }
+    }
+
+    @Test
+    public void testCheckUid_pass() {
+        when(mPackageManagerInternal.getPackageUid(TEST_PACKAGE, 0, mContext.getUserId()))
+                .thenReturn(Binder.getCallingUid());
+        try {
+            mStatusBarManagerService.requestAddTile(
+                    TEST_COMPONENT,
+                    TILE_LABEL,
+                    mIcon,
+                    mContext.getUserId(),
+                    new Callback()
+            );
+        } catch (SecurityException e) {
+            fail("No SecurityException should be thrown");
+        }
+    }
+
+    @Test
+    public void testCheckUid_pass_differentUser() {
+        int otherUserUid = UserHandle.getUid(17, UserHandle.getAppId(Binder.getCallingUid()));
+        when(mPackageManagerInternal.getPackageUid(TEST_PACKAGE, 0, mContext.getUserId()))
+                .thenReturn(otherUserUid);
+        try {
+            mStatusBarManagerService.requestAddTile(
+                    TEST_COMPONENT,
+                    TILE_LABEL,
+                    mIcon,
+                    mContext.getUserId(),
+                    new Callback()
+            );
+        } catch (SecurityException e) {
+            fail("No SecurityException should be thrown");
+        }
+    }
+
+    @Test
+    public void testCheckUid_fail() {
+        when(mPackageManagerInternal.getPackageUid(TEST_PACKAGE, 0, mContext.getUserId()))
+                .thenReturn(Binder.getCallingUid() + 1);
+        try {
+            mStatusBarManagerService.requestAddTile(
+                    TEST_COMPONENT,
+                    TILE_LABEL,
+                    mIcon,
+                    mContext.getUserId(),
+                    new Callback()
+            );
+            fail("Should throw SecurityException");
+        } catch (SecurityException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void testCurrentUser_fail() {
+        mockUidCheck();
+        int user = 0;
+        when(mActivityManagerInternal.getCurrentUserId()).thenReturn(user + 1);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testCurrentUser_pass() {
+        mockUidCheck();
+        int user = 0;
+        when(mActivityManagerInternal.getCurrentUserId()).thenReturn(user);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertNotEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testValidComponent_fail_noComponentFound() {
+        int user = 10;
+        mockUidCheck();
+        mockCurrentUserCheck(user);
+        IntentMatcher im = new IntentMatcher(
+                new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT));
+        when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0),
+                eq(user), anyInt())).thenReturn(null);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, callback.mUserResponse);
+    }
+
+    @Test
+    public void testValidComponent_fail_notEnabled() {
+        int user = 10;
+        mockUidCheck();
+        mockCurrentUserCheck(user);
+
+        ResolveInfo r = makeResolveInfo();
+        r.serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE;
+
+        IntentMatcher im = new IntentMatcher(
+                new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT));
+        when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0),
+                eq(user), anyInt())).thenReturn(r);
+        when(mPackageManagerInternal.getComponentEnabledSetting(TEST_COMPONENT,
+                Binder.getCallingUid(), user)).thenReturn(
+                PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, callback.mUserResponse);
+    }
+
+    @Test
+    public void testValidComponent_fail_noPermission() {
+        int user = 10;
+        mockUidCheck();
+        mockCurrentUserCheck(user);
+
+        ResolveInfo r = makeResolveInfo();
+
+        IntentMatcher im = new IntentMatcher(
+                new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT));
+        when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0),
+                eq(user), anyInt())).thenReturn(r);
+        when(mPackageManagerInternal.getComponentEnabledSetting(TEST_COMPONENT,
+                Binder.getCallingUid(), user)).thenReturn(
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, callback.mUserResponse);
+    }
+
+    @Test
+    public void testValidComponent_fail_notExported() {
+        int user = 10;
+        mockUidCheck();
+        mockCurrentUserCheck(user);
+
+        ResolveInfo r = makeResolveInfo();
+        r.serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE;
+        r.serviceInfo.exported = false;
+
+        IntentMatcher im = new IntentMatcher(
+                new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT));
+        when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0),
+                eq(user), anyInt())).thenReturn(r);
+        when(mPackageManagerInternal.getComponentEnabledSetting(TEST_COMPONENT,
+                Binder.getCallingUid(), user)).thenReturn(
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, callback.mUserResponse);
+    }
+
+    @Test
+    public void testValidComponent_pass() {
+        int user = 10;
+        mockUidCheck();
+        mockCurrentUserCheck(user);
+
+        ResolveInfo r = makeResolveInfo();
+        r.serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE;
+        r.serviceInfo.exported = true;
+
+        IntentMatcher im = new IntentMatcher(
+                new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT));
+        when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0),
+                eq(user), anyInt())).thenReturn(r);
+        when(mPackageManagerInternal.getComponentEnabledSetting(TEST_COMPONENT,
+                Binder.getCallingUid(), user)).thenReturn(
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertNotEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testAppInForeground_fail() {
+        int user = 10;
+        mockUidCheck();
+        mockCurrentUserCheck(user);
+        mockComponentInfo(user);
+
+        when(mActivityManagerInternal.getUidProcessState(Binder.getCallingUid())).thenReturn(
+                PROCESS_STATE_FOREGROUND_SERVICE);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testAppInForeground_pass() {
+        int user = 10;
+        mockUidCheck();
+        mockCurrentUserCheck(user);
+        mockComponentInfo(user);
+
+        when(mActivityManagerInternal.getUidProcessState(Binder.getCallingUid())).thenReturn(
+                PROCESS_STATE_TOP);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertNotEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testRequestToStatusBar() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user,
+                new Callback());
+
+        verify(mMockStatusBar).requestAddTile(
+                eq(TEST_COMPONENT),
+                eq(APP_NAME),
+                eq(TILE_LABEL),
+                eq(mIcon),
+                any()
+        );
+    }
+
+    @Test
+    public void testRequestInProgress_samePackage() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user,
+                new Callback());
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testRequestInProgress_differentPackage() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+        ComponentName otherComponent = new ComponentName("a", "b");
+        mockUidCheck(otherComponent.getPackageName());
+        mockComponentInfo(user, otherComponent);
+
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user,
+                new Callback());
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(otherComponent, TILE_LABEL, mIcon, user, callback);
+
+        assertNotEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testResponseForwardedToCallback_tileAdded() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        verify(mMockStatusBar).requestAddTile(
+                eq(TEST_COMPONENT),
+                eq(APP_NAME),
+                eq(TILE_LABEL),
+                eq(mIcon),
+                mAddTileResultCallbackCaptor.capture()
+        );
+
+        mAddTileResultCallbackCaptor.getValue().onTileRequest(
+                StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED);
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED, callback.mUserResponse);
+    }
+
+    @Test
+    public void testResponseForwardedToCallback_tileNotAdded() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        verify(mMockStatusBar).requestAddTile(
+                eq(TEST_COMPONENT),
+                eq(APP_NAME),
+                eq(TILE_LABEL),
+                eq(mIcon),
+                mAddTileResultCallbackCaptor.capture()
+        );
+
+        mAddTileResultCallbackCaptor.getValue().onTileRequest(
+                StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED);
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testResponseForwardedToCallback_tileAlreadyAdded() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        verify(mMockStatusBar).requestAddTile(
+                eq(TEST_COMPONENT),
+                eq(APP_NAME),
+                eq(TILE_LABEL),
+                eq(mIcon),
+                mAddTileResultCallbackCaptor.capture()
+        );
+
+        mAddTileResultCallbackCaptor.getValue().onTileRequest(
+                StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED);
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testResponseForwardedToCallback_dialogDismissed() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        verify(mMockStatusBar).requestAddTile(
+                eq(TEST_COMPONENT),
+                eq(APP_NAME),
+                eq(TILE_LABEL),
+                eq(mIcon),
+                mAddTileResultCallbackCaptor.capture()
+        );
+
+        mAddTileResultCallbackCaptor.getValue().onTileRequest(
+                StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED);
+        // This gets translated to TILE_NOT_ADDED
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testInstaDenialAfterManyDenials() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+
+        for (int i = 0; i < TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user,
+                    new Callback());
+
+            verify(mMockStatusBar, times(i + 1)).requestAddTile(
+                    eq(TEST_COMPONENT),
+                    eq(APP_NAME),
+                    eq(TILE_LABEL),
+                    eq(mIcon),
+                    mAddTileResultCallbackCaptor.capture()
+            );
+            mAddTileResultCallbackCaptor.getValue().onTileRequest(
+                    StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED);
+        }
+
+        Callback callback = new Callback();
+        mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback);
+
+        // Only called MAX_NUM_DENIALS times
+        verify(mMockStatusBar, times(TileRequestTracker.MAX_NUM_DENIALS)).requestAddTile(
+                any(),
+                any(),
+                any(),
+                any(),
+                mAddTileResultCallbackCaptor.capture()
+        );
+        assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
+                callback.mUserResponse);
+    }
+
+    @Test
+    public void testDialogDismissalNotCountingAgainstDenials() throws RemoteException {
+        int user = 10;
+        mockEverything(user);
+
+        for (int i = 0; i < TileRequestTracker.MAX_NUM_DENIALS * 2; i++) {
+            mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user,
+                    new Callback());
+
+            verify(mMockStatusBar, times(i + 1)).requestAddTile(
+                    eq(TEST_COMPONENT),
+                    eq(APP_NAME),
+                    eq(TILE_LABEL),
+                    eq(mIcon),
+                    mAddTileResultCallbackCaptor.capture()
+            );
+            mAddTileResultCallbackCaptor.getValue().onTileRequest(
+                    StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED);
+        }
+    }
+
+    private void mockUidCheck() {
+        mockUidCheck(TEST_PACKAGE);
+    }
+
+    private void mockUidCheck(String packageName) {
+        when(mPackageManagerInternal.getPackageUid(eq(packageName), anyInt(), anyInt()))
+                .thenReturn(Binder.getCallingUid());
+    }
+
+    private void mockCurrentUserCheck(int user) {
+        when(mActivityManagerInternal.getCurrentUserId()).thenReturn(user);
+    }
+
+    private void mockComponentInfo(int user) {
+        mockComponentInfo(user, TEST_COMPONENT);
+    }
+
+    private ResolveInfo makeResolveInfo() {
+        ResolveInfo r = new ResolveInfo();
+        r.serviceInfo = new ServiceInfo();
+        r.serviceInfo.applicationInfo = mApplicationInfo;
+        return r;
+    }
+
+    private void mockComponentInfo(int user, ComponentName componentName) {
+        ResolveInfo r = makeResolveInfo();
+        r.serviceInfo.exported = true;
+        r.serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE;
+
+        IntentMatcher im = new IntentMatcher(
+                new Intent(TileService.ACTION_QS_TILE).setComponent(componentName));
+        when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0),
+                eq(user), anyInt())).thenReturn(r);
+        when(mPackageManagerInternal.getComponentEnabledSetting(componentName,
+                Binder.getCallingUid(), user)).thenReturn(
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+    }
+
+    private void mockProcessState() {
+        when(mActivityManagerInternal.getUidProcessState(Binder.getCallingUid())).thenReturn(
+                PROCESS_STATE_TOP);
+    }
+
+    private void mockEverything(int user) {
+        mockUidCheck();
+        mockCurrentUserCheck(user);
+        mockComponentInfo(user);
+        mockProcessState();
+    }
+
+    private static class Callback extends IAddTileResultCallback.Stub {
+        int mUserResponse = -1;
+
+        @Override
+        public void onTileRequest(int userResponse) throws RemoteException {
+            if (mUserResponse != -1) {
+                throw new IllegalStateException(
+                        "Setting response to " + userResponse + " but it already has "
+                                + mUserResponse);
+            }
+            mUserResponse = userResponse;
+        }
+    }
+
+    private static class IntentMatcher implements ArgumentMatcher<Intent> {
+        private final Intent mIntent;
+
+        IntentMatcher(Intent intent) {
+            mIntent = intent;
+        }
+
+        @Override
+        public boolean matches(Intent argument) {
+            return argument != null && argument.filterEquals(mIntent);
+        }
+
+        @Override
+        public String toString() {
+            return "Expected: " + mIntent;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/statusbar/TileRequestTrackerTest.java b/services/tests/servicestests/src/com/android/server/statusbar/TileRequestTrackerTest.java
new file mode 100644
index 0000000..dac6df9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/statusbar/TileRequestTrackerTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.statusbar;
+
+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 android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.UserHandle;
+
+import androidx.test.InstrumentationRegistry;
+
+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.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class TileRequestTrackerTest {
+
+    private static final String TEST_PACKAGE = "test_pkg";
+    private static final String TEST_SERVICE = "test_svc";
+    private static final String TEST_SERVICE_OTHER = "test_svc_other";
+    private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PACKAGE,
+            TEST_SERVICE);
+    private static final ComponentName TEST_COMPONENT_OTHER = new ComponentName(TEST_PACKAGE,
+            TEST_SERVICE_OTHER);
+    private static final ComponentName TEST_COMPONENT_OTHER_PACKAGE = new ComponentName("other",
+            TEST_SERVICE);
+    private static final int USER_ID = 0;
+    private static final int USER_ID_OTHER = 10;
+    private static final int APP_UID = 12345;
+    private static final int USER_UID = UserHandle.getUid(USER_ID, APP_UID);
+    private static final int USER_OTHER_UID = UserHandle.getUid(USER_ID_OTHER, APP_UID);
+
+    @Rule
+    public final NoBroadcastContextWrapper mContext =
+            new NoBroadcastContextWrapper(InstrumentationRegistry.getContext());
+
+    private TileRequestTracker mTileRequestTracker;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mTileRequestTracker = new TileRequestTracker(mContext);
+    }
+
+    @Test
+    public void testBroadcastReceiverRegistered() {
+        NoBroadcastContextWrapper.BroadcastReceiverRegistration reg = getReceiverRegistration();
+
+        assertEquals(UserHandle.ALL, reg.mUser);
+        assertNull(reg.mBroadcastPermission);
+        assertNotNull(reg.mReceiver);
+
+        IntentFilter filter = reg.mIntentFilter;
+        assertEquals(2, filter.countActions());
+        assertTrue(filter.hasAction(Intent.ACTION_PACKAGE_REMOVED));
+        assertTrue(filter.hasAction(Intent.ACTION_PACKAGE_DATA_CLEARED));
+        assertTrue(filter.hasDataScheme("package"));
+    }
+
+    @Test
+    public void testNoDenialsFromStart() {
+        // Certainly not an exhaustive test
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT));
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID_OTHER, TEST_COMPONENT));
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT_OTHER));
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID_OTHER, TEST_COMPONENT_OTHER));
+    }
+
+    @Test
+    public void testNoDenialBeforeMax() {
+        for (int i = 1; i < TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+        }
+
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT));
+    }
+
+    @Test
+    public void testDenialOnMax() {
+        for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+        }
+        assertTrue(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT));
+    }
+
+    @Test
+    public void testDenialPerUser() {
+        for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+        }
+
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID_OTHER, TEST_COMPONENT));
+    }
+
+    @Test
+    public void testDenialPerComponent() {
+        for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+        }
+
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT_OTHER));
+    }
+
+    @Test
+    public void testPackageUninstallRemovesDenials_allComponents() {
+        for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT_OTHER);
+        }
+
+        Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
+        intent.putExtra(Intent.EXTRA_UID, USER_UID);
+        intent.setData(Uri.parse("package:" + TEST_PACKAGE));
+        getReceiverRegistration().mReceiver.onReceive(mContext, intent);
+
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT));
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT_OTHER));
+    }
+
+    @Test
+    public void testPackageUninstallRemoveDenials_differentUsers() {
+        for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+            mTileRequestTracker.addDenial(USER_ID_OTHER, TEST_COMPONENT);
+        }
+
+        Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
+        intent.putExtra(Intent.EXTRA_UID, USER_OTHER_UID);
+        intent.setData(Uri.parse("package:" + TEST_PACKAGE));
+        getReceiverRegistration().mReceiver.onReceive(mContext, intent);
+
+        // User 0 package was not removed
+        assertTrue(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT));
+        // User 10 package was removed
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID_OTHER, TEST_COMPONENT));
+    }
+
+    @Test
+    public void testPackageUninstallRemoveDenials_differentPackages() {
+        for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT_OTHER_PACKAGE);
+        }
+
+        Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
+        intent.putExtra(Intent.EXTRA_UID, USER_UID);
+        intent.setData(Uri.parse("package:" + TEST_PACKAGE));
+        getReceiverRegistration().mReceiver.onReceive(mContext, intent);
+
+        // Package TEST_PACKAGE removed
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT));
+        // Package "other" not removed
+        assertTrue(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT_OTHER_PACKAGE));
+    }
+
+    @Test
+    public void testPackageUpdateDoesntRemoveDenials() {
+        for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+        }
+
+        Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
+        intent.putExtra(Intent.EXTRA_REPLACING, true);
+        intent.putExtra(Intent.EXTRA_UID, USER_UID);
+        intent.setData(Uri.parse("package:" + TEST_PACKAGE));
+        getReceiverRegistration().mReceiver.onReceive(mContext, intent);
+
+        assertTrue(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT));
+    }
+
+    @Test
+    public void testClearPackageDataRemovesDenials() {
+        for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) {
+            mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT);
+        }
+
+        Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED);
+        intent.putExtra(Intent.EXTRA_UID, USER_UID);
+        intent.setData(Uri.parse("package:" + TEST_PACKAGE));
+        getReceiverRegistration().mReceiver.onReceive(mContext, intent);
+
+        assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT));
+    }
+
+    private NoBroadcastContextWrapper.BroadcastReceiverRegistration getReceiverRegistration() {
+        assertEquals(1, mContext.mRegistrationList.size());
+        return mContext.mRegistrationList.get(0);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index 4dcd633..add4cda 100644
--- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -62,12 +62,15 @@
     private static final String LOG_TAG = "SystemConfigTest";
 
     private SystemConfig mSysConfig;
+    private File mFooJar;
 
     @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
 
     @Before
     public void setUp() throws Exception {
         mSysConfig = new SystemConfigTestClass();
+        mFooJar = createTempFile(
+                mTemporaryFolder.getRoot().getCanonicalFile(), "foo.jar", "JAR");
     }
 
     /**
@@ -340,7 +343,7 @@
                 "<permissions>\n"
                 + "    <library \n"
                 + "        name=\"foo\"\n"
-                + "        file=\"foo.jar\"\n"
+                + "        file=\"" + mFooJar + "\"\n"
                 + "        on-bootclasspath-before=\"10\"\n"
                 + "        on-bootclasspath-since=\"20\"\n"
                 + "     />\n\n"
@@ -362,7 +365,7 @@
                 "<permissions>\n"
                         + "    <updatable-library \n"
                         + "        name=\"foo\"\n"
-                        + "        file=\"foo.jar\"\n"
+                        + "        file=\"" + mFooJar + "\"\n"
                         + "        on-bootclasspath-before=\"10\"\n"
                         + "        on-bootclasspath-since=\"20\"\n"
                         + "     />\n\n"
@@ -384,7 +387,7 @@
                 "<permissions>\n"
                 + "    <library \n"
                 + "        name=\"foo\"\n"
-                + "        file=\"foo.jar\"\n"
+                + "        file=\"" + mFooJar + "\"\n"
                 + "        min-device-sdk=\"30\"\n"
                 + "     />\n\n"
                 + " </permissions>";
@@ -402,7 +405,7 @@
                 "<permissions>\n"
                 + "    <library \n"
                 + "        name=\"foo\"\n"
-                + "        file=\"foo.jar\"\n"
+                + "        file=\"" + mFooJar + "\"\n"
                 + "        min-device-sdk=\"" + Build.VERSION.SDK_INT + "\"\n"
                 + "     />\n\n"
                 + " </permissions>";
@@ -420,7 +423,7 @@
                 "<permissions>\n"
                 + "    <library \n"
                 + "        name=\"foo\"\n"
-                + "        file=\"foo.jar\"\n"
+                + "        file=\"" + mFooJar + "\"\n"
                 + "        min-device-sdk=\"" + (Build.VERSION.SDK_INT + 1) + "\"\n"
                 + "     />\n\n"
                 + " </permissions>";
@@ -438,7 +441,7 @@
                 "<permissions>\n"
                 + "    <library \n"
                 + "        name=\"foo\"\n"
-                + "        file=\"foo.jar\"\n"
+                + "        file=\"" + mFooJar + "\"\n"
                 + "        max-device-sdk=\"30\"\n"
                 + "     />\n\n"
                 + " </permissions>";
@@ -456,7 +459,7 @@
                 "<permissions>\n"
                 + "    <library \n"
                 + "        name=\"foo\"\n"
-                + "        file=\"foo.jar\"\n"
+                + "        file=\"" + mFooJar + "\"\n"
                 + "        max-device-sdk=\"" + Build.VERSION.SDK_INT + "\"\n"
                 + "     />\n\n"
                 + " </permissions>";
@@ -474,7 +477,7 @@
                 "<permissions>\n"
                 + "    <library \n"
                 + "        name=\"foo\"\n"
-                + "        file=\"foo.jar\"\n"
+                + "        file=\"" + mFooJar + "\"\n"
                 + "        max-device-sdk=\"" + (Build.VERSION.SDK_INT + 1) + "\"\n"
                 + "     />\n\n"
                 + " </permissions>";
@@ -507,7 +510,7 @@
      * @param folder   pre-existing subdirectory of mTemporaryFolder to put the file
      * @param fileName name of the file (e.g. filename.xml) to create
      * @param contents contents to write to the file
-     * @return the folder containing the newly created file (not the file itself!)
+     * @return the newly created file
      */
     private File createTempFile(File folder, String fileName, String contents)
             throws IOException {
@@ -523,13 +526,13 @@
             Log.d(LOG_TAG, input.nextLine());
         }
 
-        return folder;
+        return file;
     }
 
     private void assertFooIsOnlySharedLibrary() {
         assertThat(mSysConfig.getSharedLibraries().size()).isEqualTo(1);
         SystemConfig.SharedLibraryEntry entry = mSysConfig.getSharedLibraries().get("foo");
         assertThat(entry.name).isEqualTo("foo");
-        assertThat(entry.filename).isEqualTo("foo.jar");
+        assertThat(entry.filename).isEqualTo(mFooJar.toString());
     }
 }
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 c0959d3..2ea7fda 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -22,6 +22,9 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+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_CRASHING_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
@@ -92,6 +95,7 @@
         final ActivityRecord activity = createActivityRecord(dc);
 
         mDc.prepareAppTransition(TRANSIT_OPEN);
+        mDc.prepareAppTransition(TRANSIT_KEYGUARD_OCCLUDE);
         mDc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY);
         mDc.mOpeningApps.add(activity);
         assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY,
@@ -102,6 +106,22 @@
     }
 
     @Test
+    public void testKeyguardUnoccludeOcclude() {
+        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
+        final ActivityRecord activity = createActivityRecord(dc);
+
+        mDc.prepareAppTransition(TRANSIT_KEYGUARD_UNOCCLUDE);
+        mDc.prepareAppTransition(TRANSIT_KEYGUARD_OCCLUDE);
+        mDc.mOpeningApps.add(activity);
+        assertEquals(TRANSIT_NONE,
+                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
+                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
+
+    }
+
+    @Test
     public void testKeyguardKeep() {
         final DisplayContent dc = createNewDisplay(Display.STATE_ON);
         final ActivityRecord activity = createActivityRecord(dc);
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 1fab89e..f728324 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -3811,14 +3811,21 @@
     }
 
     /**
-     * Returns the phone number for the given {@code subId} and {@code source},
+     * Returns the phone number for the given {@code subscriptionId} and {@code source},
      * or an empty string if not available.
      *
-     * <p>Requires Permission:
-     * {@link android.Manifest.permission#READ_PHONE_NUMBERS READ_PHONE_NUMBERS}, or
-     * READ_PRIVILEGED_PHONE_STATE permission (can only be granted to apps preloaded on device),
-     * or that the calling app has carrier privileges
-     * (see {@link TelephonyManager#hasCarrierPrivileges}).
+     * <p>General apps that need to know the phone number should use {@link #getPhoneNumber(int)}
+     * instead. This API may be suitable specific apps that needs to know the phone number from
+     * a specific source. For example, a carrier app needs to know exactly what's on
+     * {@link #PHONE_NUMBER_SOURCE_UICC UICC} and decide if the previously set phone number
+     * of source {@link #PHONE_NUMBER_SOURCE_CARRIER carrier} should be updated.
+     *
+     * <p>Note the assumption is that one subscription (which usually means one SIM) has
+     * only one phone number. The multiple sources backup each other so hopefully at least one
+     * is availavle. For example, for a carrier that doesn't typically set phone numbers
+     * on {@link #PHONE_NUMBER_SOURCE_UICC UICC}, the source {@link #PHONE_NUMBER_SOURCE_IMS IMS}
+     * 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.
      *
      * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID}
      *                       for the default one.
@@ -3831,10 +3838,10 @@
      * @see #PHONE_NUMBER_SOURCE_CARRIER
      * @see #PHONE_NUMBER_SOURCE_IMS
      */
-    @SuppressAutoDoc // No support for carrier privileges (b/72967236)
     @RequiresPermission(anyOf = {
             android.Manifest.permission.READ_PHONE_NUMBERS,
             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            "carrier privileges",
     })
     @NonNull
     public String getPhoneNumber(int subscriptionId, @PhoneNumberSource int source) {
@@ -3863,16 +3870,14 @@
      * Returns the phone number for the given {@code subId}, or an empty string if
      * not available.
      *
+     * <p>This API is suitable for general apps that needs to know the phone number.
+     * For specific apps that needs to know the phone number provided by a specific source,
+     * {@link #getPhoneNumber(int, int)} may be suitable.
+     *
      * <p>This API is built up on {@link #getPhoneNumber(int, int)}, but picks
      * from available sources in the following order: {@link #PHONE_NUMBER_SOURCE_CARRIER}
      * > {@link #PHONE_NUMBER_SOURCE_UICC} > {@link #PHONE_NUMBER_SOURCE_IMS}.
      *
-     * <p>Requires Permission:
-     * {@link android.Manifest.permission#READ_PHONE_NUMBERS READ_PHONE_NUMBERS}, or
-     * READ_PRIVILEGED_PHONE_STATE permission (can only be granted to apps preloaded on device),
-     * or that the calling app has carrier privileges
-     * (see {@link TelephonyManager#hasCarrierPrivileges}).
-     *
      * @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.
@@ -3880,10 +3885,10 @@
      * @throws SecurityException if the caller doesn't have permissions required.
      * @see #getPhoneNumber(int, int)
      */
-    @SuppressAutoDoc // No support for carrier privileges (b/72967236)
     @RequiresPermission(anyOf = {
             android.Manifest.permission.READ_PHONE_NUMBERS,
             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            "carrier privileges",
     })
     @NonNull
     public String getPhoneNumber(int subscriptionId) {
@@ -3908,8 +3913,8 @@
      * {@link #PHONE_NUMBER_SOURCE_CARRIER carrier}.
      * Sets an empty string to remove the previously set phone number.
      *
-     * <p>Requires Permission: the calling app has carrier privileges
-     * (see {@link TelephonyManager#hasCarrierPrivileges}).
+     * <p>The API is suitable for carrier apps to provide a phone number, for example when
+     * it's not possible to update {@link #PHONE_NUMBER_SOURCE_UICC UICC} directly.
      *
      * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID}
      *                       for the default one.
@@ -3918,6 +3923,7 @@
      * @throws NullPointerException if {@code number} is {@code null}.
      * @throws SecurityException if the caller doesn't have permissions required.
      */
+    @RequiresPermission("carrier privileges")
     public void setCarrierPhoneNumber(int subscriptionId, @NonNull String number) {
         if (subscriptionId == DEFAULT_SUBSCRIPTION_ID) {
             subscriptionId = getDefaultSubscriptionId();
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 5f21e87..0267b68 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -117,6 +117,7 @@
 import com.android.internal.telephony.ISub;
 import com.android.internal.telephony.ITelephony;
 import com.android.internal.telephony.IUpdateAvailableNetworksCallback;
+import com.android.internal.telephony.IccLogicalChannelRequest;
 import com.android.internal.telephony.OperatorInfo;
 import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.telephony.RILConstants;
@@ -6774,8 +6775,13 @@
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                return telephony.iccOpenLogicalChannelByPort(slotIndex, DEFAULT_PORT_INDEX,
-                         getOpPackageName(), aid, p2);
+                IccLogicalChannelRequest request = new IccLogicalChannelRequest();
+                request.slotIndex = slotIndex;
+                request.aid = aid;
+                request.p2 = p2;
+                request.callingPackage = getOpPackageName();
+                request.binder = new Binder();
+                return telephony.iccOpenLogicalChannel(request);
             }
         } catch (RemoteException ex) {
         } catch (NullPointerException ex) {
@@ -6842,8 +6848,15 @@
     public IccOpenLogicalChannelResponse iccOpenLogicalChannel(int subId, String AID, int p2) {
         try {
             ITelephony telephony = getITelephony();
-            if (telephony != null)
-                return telephony.iccOpenLogicalChannel(subId, getOpPackageName(), AID, p2);
+            if (telephony != null) {
+                IccLogicalChannelRequest request = new IccLogicalChannelRequest();
+                request.subId = subId;
+                request.callingPackage = getOpPackageName();
+                request.aid = AID;
+                request.p2 = p2;
+                request.binder = new Binder();
+                return telephony.iccOpenLogicalChannel(request);
+            }
         } catch (RemoteException ex) {
         } catch (NullPointerException ex) {
         }
@@ -6873,8 +6886,10 @@
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                return telephony.iccCloseLogicalChannelByPort(slotIndex, DEFAULT_PORT_INDEX,
-                         channel);
+                IccLogicalChannelRequest request = new IccLogicalChannelRequest();
+                request.slotIndex = slotIndex;
+                request.channel = channel;
+                return telephony.iccCloseLogicalChannel(request);
             }
         } catch (RemoteException ex) {
         } catch (NullPointerException ex) {
@@ -6917,8 +6932,12 @@
     public boolean iccCloseLogicalChannel(int subId, int channel) {
         try {
             ITelephony telephony = getITelephony();
-            if (telephony != null)
-                return telephony.iccCloseLogicalChannel(subId, channel);
+            if (telephony != null) {
+                IccLogicalChannelRequest request = new IccLogicalChannelRequest();
+                request.subId = subId;
+                request.channel = channel;
+                return telephony.iccCloseLogicalChannel(request);
+            }
         } catch (RemoteException ex) {
         } catch (NullPointerException ex) {
         }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 00b57e6..d5c9ec4 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -67,6 +67,7 @@
 import com.android.internal.telephony.CellNetworkScanResult;
 import com.android.internal.telephony.IBooleanConsumer;
 import com.android.internal.telephony.ICallForwardingInfoCallback;
+import com.android.internal.telephony.IccLogicalChannelRequest;
 import com.android.internal.telephony.IImsStateCallback;
 import com.android.internal.telephony.IIntegerConsumer;
 import com.android.internal.telephony.INumberVerificationCallback;
@@ -584,59 +585,24 @@
     void setCellInfoListRate(int rateInMillis);
 
     /**
-     * Opens a logical channel to the ICC card using the physical slot index and port index.
-     *
-     * Input parameters equivalent to TS 27.007 AT+CCHO command.
-     *
-     * @param slotIndex The physical slot index of the target ICC card
-     * @param portIndex The unique index referring to a port belonging to the SIM slot
-     * @param callingPackage the name of the package making the call.
-     * @param AID Application id. See ETSI 102.221 and 101.220.
-     * @param p2 P2 parameter (described in ISO 7816-4).
-     * @return an IccOpenLogicalChannelResponse object.
-     */
-    IccOpenLogicalChannelResponse iccOpenLogicalChannelByPort(
-            int slotIndex, int portIndex, String callingPackage, String AID, int p2);
-
-    /**
      * Opens a logical channel to the ICC card.
      *
      * Input parameters equivalent to TS 27.007 AT+CCHO command.
      *
-     * @param subId The subscription to use.
-     * @param callingPackage the name of the package making the call.
-     * @param AID Application id. See ETSI 102.221 and 101.220.
-     * @param p2 P2 parameter (described in ISO 7816-4).
+     * @param request the parcelable used to indicate how to open the logical channel.
      * @return an IccOpenLogicalChannelResponse object.
      */
-    IccOpenLogicalChannelResponse iccOpenLogicalChannel(
-            int subId, String callingPackage, String AID, int p2);
-
-    /**
-     * Closes a previously opened logical channel to the ICC card using the physical slot index and port index.
-     *
-     * Input parameters equivalent to TS 27.007 AT+CCHC command.
-     *
-     * @param slotIndex The physical slot index of the target ICC card
-     * @param portIndex The unique index referring to a port belonging to the SIM slot
-     * @param channel is the channel id to be closed as returned by a
-     *            successful iccOpenLogicalChannel.
-     * @return true if the channel was closed successfully.
-     */
-    boolean iccCloseLogicalChannelByPort(int slotIndex, int portIndex, int channel);
+    IccOpenLogicalChannelResponse iccOpenLogicalChannel(in IccLogicalChannelRequest request);
 
     /**
      * Closes a previously opened logical channel to the ICC card.
      *
      * Input parameters equivalent to TS 27.007 AT+CCHC command.
      *
-     * @param subId The subscription to use.
-     * @param channel is the channel id to be closed as returned by a
-     *            successful iccOpenLogicalChannel.
+     * @param request the parcelable used to indicate how to close the logical channel.
      * @return true if the channel was closed successfully.
      */
-    @UnsupportedAppUsage(trackingBug = 171933273)
-    boolean iccCloseLogicalChannel(int subId, int channel);
+    boolean iccCloseLogicalChannel(in IccLogicalChannelRequest request);
 
     /**
      * Transmit an APDU to the ICC card over a logical channel using the physical slot index and port index.
diff --git a/telephony/java/com/android/internal/telephony/IccLogicalChannelRequest.aidl b/telephony/java/com/android/internal/telephony/IccLogicalChannelRequest.aidl
new file mode 100644
index 0000000..a84e752
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/IccLogicalChannelRequest.aidl
@@ -0,0 +1,52 @@
+/*
+** 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.
+*/
+
+package com.android.internal.telephony;
+
+import android.os.IBinder;
+
+/**
+ * A request to open or close a logical channel to the ICC card.
+ *
+ * @hide
+ */
+@JavaDerive(toString=true, equals=true)
+parcelable IccLogicalChannelRequest {
+
+    /** Subscription id. */
+    int subId = -1;
+
+    /** Physical slot index of the ICC card. */
+    int slotIndex = -1;
+
+    /** The unique index referring to a port belonging to the ICC card slot. */
+    int portIndex = 0;
+
+    /** Package name for the calling app, used only when open channel. */
+    @nullable String callingPackage;
+
+    /** Application id, used only when open channel. */
+    @nullable String aid;
+
+    /** The P2 parameter described in ISO 7816-4, used only when open channel. */
+    int p2 = 0;
+
+    /** Channel number */
+    int channel = -1;
+
+    /** A IBinder object for server side to check if the request client is still living. */
+    @nullable IBinder binder;
+}
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 8a0af2d..5628321 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -179,7 +179,7 @@
 
         doReturn(mUnderlyingNetworkController)
                 .when(mDeps)
-                .newUnderlyingNetworkController(any(), any(), any(), any());
+                .newUnderlyingNetworkController(any(), any(), any(), any(), any());
         doReturn(mWakeLock)
                 .when(mDeps)
                 .newWakeLock(eq(mContext), eq(PowerManager.PARTIAL_WAKE_LOCK), any());
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
new file mode 100644
index 0000000..2e1aab6
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
@@ -0,0 +1,383 @@
+/*
+ * 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.vcn.routeselection;
+
+import static android.net.vcn.VcnUnderlyingNetworkPriority.NETWORK_QUALITY_OK;
+
+import static com.android.server.vcn.VcnTestUtils.setupSystemService;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_ANY;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.calculatePriorityClass;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesCellPriorityRule;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesPriorityRule;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesWifiPriorityRule;
+import static com.android.server.vcn.routeselection.UnderlyingNetworkControllerTest.getLinkPropertiesWithName;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnCellUnderlyingNetworkPriority;
+import android.net.vcn.VcnGatewayConnectionConfig;
+import android.net.vcn.VcnManager;
+import android.net.vcn.VcnWifiUnderlyingNetworkPriority;
+import android.os.ParcelUuid;
+import android.os.PersistableBundle;
+import android.os.test.TestLooper;
+import android.telephony.TelephonyManager;
+import android.util.ArraySet;
+
+import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.VcnNetworkProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Set;
+import java.util.UUID;
+
+public class NetworkPriorityClassifierTest {
+    private static final String SSID = "TestWifi";
+    private static final String SSID_OTHER = "TestWifiOther";
+    private static final String PLMN_ID = "123456";
+    private static final String PLMN_ID_OTHER = "234567";
+
+    private static final int SUB_ID = 1;
+    private static final int WIFI_RSSI = -60;
+    private static final int WIFI_RSSI_HIGH = -50;
+    private static final int WIFI_RSSI_LOW = -80;
+    private static final int CARRIER_ID = 1;
+    private static final int CARRIER_ID_OTHER = 2;
+
+    private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
+
+    private static final NetworkCapabilities WIFI_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .setSignalStrength(WIFI_RSSI)
+                    .setSsid(SSID)
+                    .build();
+
+    private static final TelephonyNetworkSpecifier TEL_NETWORK_SPECIFIER =
+            new TelephonyNetworkSpecifier.Builder().setSubscriptionId(SUB_ID).build();
+    private static final NetworkCapabilities CELL_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                    .setSubscriptionIds(Set.of(SUB_ID))
+                    .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
+                    .build();
+
+    private static final LinkProperties LINK_PROPERTIES = getLinkPropertiesWithName("test_iface");
+
+    @Mock private Network mNetwork;
+    @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
+    @Mock private TelephonyManager mTelephonyManager;
+
+    private TestLooper mTestLooper;
+    private VcnContext mVcnContext;
+    private UnderlyingNetworkRecord mWifiNetworkRecord;
+    private UnderlyingNetworkRecord mCellNetworkRecord;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        final Context mockContext = mock(Context.class);
+        mTestLooper = new TestLooper();
+        mVcnContext =
+                spy(
+                        new VcnContext(
+                                mockContext,
+                                mTestLooper.getLooper(),
+                                mock(VcnNetworkProvider.class),
+                                false /* isInTestMode */));
+        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
+
+        mWifiNetworkRecord =
+                new UnderlyingNetworkRecord(
+                        mNetwork,
+                        WIFI_NETWORK_CAPABILITIES,
+                        LINK_PROPERTIES,
+                        false /* isBlocked */);
+
+        mCellNetworkRecord =
+                new UnderlyingNetworkRecord(
+                        mNetwork,
+                        CELL_NETWORK_CAPABILITIES,
+                        LINK_PROPERTIES,
+                        false /* isBlocked */);
+
+        setupSystemService(
+                mockContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
+        when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
+        when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID);
+        when(mTelephonyManager.getSimSpecificCarrierId()).thenReturn(CARRIER_ID);
+    }
+
+    @Test
+    public void testMatchWithoutNotMeteredBit() {
+        final VcnWifiUnderlyingNetworkPriority wifiNetworkPriority =
+                new VcnWifiUnderlyingNetworkPriority.Builder()
+                        .setNetworkQuality(NETWORK_QUALITY_OK)
+                        .setAllowMetered(false /* allowMetered */)
+                        .build();
+
+        assertFalse(
+                checkMatchesPriorityRule(
+                        mVcnContext,
+                        wifiNetworkPriority,
+                        mWifiNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        null /* currentlySelecetd */,
+                        null /* carrierConfig */));
+    }
+
+    private void verifyMatchWifi(
+            boolean isSelectedNetwork, PersistableBundle carrierConfig, boolean expectMatch) {
+        final VcnWifiUnderlyingNetworkPriority wifiNetworkPriority =
+                new VcnWifiUnderlyingNetworkPriority.Builder()
+                        .setNetworkQuality(NETWORK_QUALITY_OK)
+                        .setAllowMetered(true /* allowMetered */)
+                        .build();
+        final UnderlyingNetworkRecord selectedNetworkRecord =
+                isSelectedNetwork ? mWifiNetworkRecord : null;
+        assertEquals(
+                expectMatch,
+                checkMatchesWifiPriorityRule(
+                        wifiNetworkPriority,
+                        mWifiNetworkRecord,
+                        selectedNetworkRecord,
+                        carrierConfig));
+    }
+
+    @Test
+    public void testMatchSelectedWifi() {
+        verifyMatchWifi(
+                true /* isSelectedNetwork */, null /* carrierConfig */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchSelectedWifiBelowRssiThreshold() {
+        final PersistableBundle carrierConfig = new PersistableBundle();
+        carrierConfig.putInt(
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY, WIFI_RSSI_HIGH);
+        carrierConfig.putInt(
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY, WIFI_RSSI_HIGH);
+
+        verifyMatchWifi(true /* isSelectedNetwork */, carrierConfig, false /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchUnselectedWifi() {
+        verifyMatchWifi(
+                false /* isSelectedNetwork */, null /* carrierConfig */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchUnselectedWifiBelowRssiThreshold() {
+        final PersistableBundle carrierConfig = new PersistableBundle();
+        carrierConfig.putInt(
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY, WIFI_RSSI_HIGH);
+
+        verifyMatchWifi(false /* isSelectedNetwork */, carrierConfig, false /* expectMatch */);
+    }
+
+    private void verifyMatchWifiWithSsid(boolean useMatchedSsid, boolean expectMatch) {
+        final String nwPrioritySsid = useMatchedSsid ? SSID : SSID_OTHER;
+        final VcnWifiUnderlyingNetworkPriority wifiNetworkPriority =
+                new VcnWifiUnderlyingNetworkPriority.Builder()
+                        .setNetworkQuality(NETWORK_QUALITY_OK)
+                        .setAllowMetered(true /* allowMetered */)
+                        .setSsid(nwPrioritySsid)
+                        .build();
+
+        assertEquals(
+                expectMatch,
+                checkMatchesWifiPriorityRule(
+                        wifiNetworkPriority,
+                        mWifiNetworkRecord,
+                        null /* currentlySelecetd */,
+                        null /* carrierConfig */));
+    }
+
+    @Test
+    public void testMatchWifiWithSsid() {
+        verifyMatchWifiWithSsid(true /* useMatchedSsid */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchWifiFailWithWrongSsid() {
+        verifyMatchWifiWithSsid(false /* useMatchedSsid */, false /* expectMatch */);
+    }
+
+    private static VcnCellUnderlyingNetworkPriority.Builder getCellNetworkPriorityBuilder() {
+        return new VcnCellUnderlyingNetworkPriority.Builder()
+                .setNetworkQuality(NETWORK_QUALITY_OK)
+                .setAllowMetered(true /* allowMetered */)
+                .setAllowRoaming(true /* allowRoaming */);
+    }
+
+    @Test
+    public void testMatchMacroCell() {
+        assertTrue(
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        getCellNetworkPriorityBuilder().build(),
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    @Test
+    public void testMatchOpportunisticCell() {
+        final VcnCellUnderlyingNetworkPriority opportunisticCellNetworkPriority =
+                getCellNetworkPriorityBuilder()
+                        .setRequireOpportunistic(true /* requireOpportunistic */)
+                        .build();
+
+        when(mSubscriptionSnapshot.isOpportunistic(SUB_ID)).thenReturn(true);
+        when(mSubscriptionSnapshot.getAllSubIdsInGroup(SUB_GROUP)).thenReturn(new ArraySet<>());
+
+        assertTrue(
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        opportunisticCellNetworkPriority,
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    private void verifyMatchMacroCellWithAllowedPlmnIds(
+            boolean useMatchedPlmnId, boolean expectMatch) {
+        final String networkPriorityPlmnId = useMatchedPlmnId ? PLMN_ID : PLMN_ID_OTHER;
+        final VcnCellUnderlyingNetworkPriority networkPriority =
+                getCellNetworkPriorityBuilder()
+                        .setAllowedPlmnIds(Set.of(networkPriorityPlmnId))
+                        .build();
+
+        assertEquals(
+                expectMatch,
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        networkPriority,
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    @Test
+    public void testMatchMacroCellWithAllowedPlmnIds() {
+        verifyMatchMacroCellWithAllowedPlmnIds(true /* useMatchedPlmnId */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchMacroCellFailWithDisallowedPlmnIds() {
+        verifyMatchMacroCellWithAllowedPlmnIds(
+                false /* useMatchedPlmnId */, false /* expectMatch */);
+    }
+
+    private void verifyMatchMacroCellWithAllowedSpecificCarrierIds(
+            boolean useMatchedCarrierId, boolean expectMatch) {
+        final int networkPriorityCarrierId = useMatchedCarrierId ? CARRIER_ID : CARRIER_ID_OTHER;
+        final VcnCellUnderlyingNetworkPriority networkPriority =
+                getCellNetworkPriorityBuilder()
+                        .setAllowedSpecificCarrierIds(Set.of(networkPriorityCarrierId))
+                        .build();
+
+        assertEquals(
+                expectMatch,
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        networkPriority,
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    @Test
+    public void testMatchMacroCellWithAllowedSpecificCarrierIds() {
+        verifyMatchMacroCellWithAllowedSpecificCarrierIds(
+                true /* useMatchedCarrierId */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchMacroCellFailWithDisallowedSpecificCarrierIds() {
+        verifyMatchMacroCellWithAllowedSpecificCarrierIds(
+                false /* useMatchedCarrierId */, false /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchWifiFailWithoutNotRoamingBit() {
+        final VcnCellUnderlyingNetworkPriority networkPriority =
+                getCellNetworkPriorityBuilder().setAllowRoaming(false /* allowRoaming */).build();
+
+        assertFalse(
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        networkPriority,
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    private void verifyCalculatePriorityClass(
+            UnderlyingNetworkRecord networkRecord, int expectedIndex) {
+        final int priorityIndex =
+                calculatePriorityClass(
+                        mVcnContext,
+                        networkRecord,
+                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_PRIORITIES,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        null /* currentlySelected */,
+                        null /* carrierConfig */);
+
+        assertEquals(expectedIndex, priorityIndex);
+    }
+
+    @Test
+    public void testCalculatePriorityClass() throws Exception {
+        verifyCalculatePriorityClass(mCellNetworkRecord, 2);
+    }
+
+    @Test
+    public void testCalculatePriorityClassFailToMatchAny() throws Exception {
+        final NetworkCapabilities nc =
+                new NetworkCapabilities.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                        .setSignalStrength(WIFI_RSSI_LOW)
+                        .setSsid(SSID)
+                        .build();
+        final UnderlyingNetworkRecord wifiNetworkRecord =
+                new UnderlyingNetworkRecord(mNetwork, nc, LINK_PROPERTIES, false /* isBlocked */);
+
+        verifyCalculatePriorityClass(wifiNetworkRecord, PRIORITY_ANY);
+    }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index c954cb8..fad9669 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -42,6 +42,7 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnGatewayConnectionConfigTest;
 import android.os.ParcelUuid;
 import android.os.test.TestLooper;
 import android.telephony.CarrierConfigManager;
@@ -145,7 +146,11 @@
 
         mUnderlyingNetworkController =
                 new UnderlyingNetworkController(
-                        mVcnContext, SUB_GROUP, mSubscriptionSnapshot, mNetworkControllerCb);
+                        mVcnContext,
+                        VcnGatewayConnectionConfigTest.buildTestConfig(),
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        mNetworkControllerCb);
     }
 
     private void resetVcnContext() {
@@ -153,7 +158,8 @@
         doNothing().when(mVcnContext).ensureRunningOnLooperThread();
     }
 
-    private static LinkProperties getLinkPropertiesWithName(String iface) {
+    // Package private for use in NetworkPriorityClassifierTest
+    static LinkProperties getLinkPropertiesWithName(String iface) {
         LinkProperties linkProperties = new LinkProperties();
         linkProperties.setInterfaceName(iface);
         return linkProperties;
@@ -182,7 +188,11 @@
                         true /* isInTestMode */);
 
         new UnderlyingNetworkController(
-                vcnContext, SUB_GROUP, mSubscriptionSnapshot, mNetworkControllerCb);
+                vcnContext,
+                VcnGatewayConnectionConfigTest.buildTestConfig(),
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mNetworkControllerCb);
 
         verify(cm)
                 .registerNetworkCallback(
@@ -345,6 +355,17 @@
         return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES);
     }
 
+    private static NetworkCapabilities buildResponseNwCaps(
+            NetworkCapabilities requestNetworkCaps, Set<Integer> netCapsSubIds) {
+        final TelephonyNetworkSpecifier telephonyNetworkSpecifier =
+                new TelephonyNetworkSpecifier.Builder()
+                        .setSubscriptionId(netCapsSubIds.iterator().next())
+                        .build();
+        return new NetworkCapabilities.Builder(requestNetworkCaps)
+                .setNetworkSpecifier(telephonyNetworkSpecifier)
+                .build();
+    }
+
     private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback(
             NetworkCapabilities networkCapabilities) {
         verify(mConnectivityManager)
@@ -355,14 +376,17 @@
 
         UnderlyingNetworkListener cb = mUnderlyingNetworkListenerCaptor.getValue();
         cb.onAvailable(mNetwork);
-        cb.onCapabilitiesChanged(mNetwork, networkCapabilities);
+
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(networkCapabilities, INITIAL_SUB_IDS);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
         cb.onLinkPropertiesChanged(mNetwork, INITIAL_LINK_PROPERTIES);
         cb.onBlockedStatusChanged(mNetwork, false /* isFalse */);
 
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        networkCapabilities,
+                        responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
@@ -373,12 +397,14 @@
     public void testRecordTrackerCallbackNotifiedForNetworkCapabilitiesChange() {
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
-        cb.onCapabilitiesChanged(mNetwork, UPDATED_NETWORK_CAPABILITIES);
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(UPDATED_NETWORK_CAPABILITIES, UPDATED_SUB_IDS);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        UPDATED_NETWORK_CAPABILITIES,
+                        responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
@@ -393,7 +419,7 @@
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        INITIAL_NETWORK_CAPABILITIES,
+                        buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS),
                         UPDATED_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
@@ -403,19 +429,21 @@
     public void testRecordTrackerCallbackNotifiedForNetworkSuspended() {
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
-        cb.onCapabilitiesChanged(mNetwork, SUSPENDED_NETWORK_CAPABILITIES);
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(SUSPENDED_NETWORK_CAPABILITIES, UPDATED_SUB_IDS);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        SUSPENDED_NETWORK_CAPABILITIES,
+                        responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb, times(1))
                 .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
         // onSelectedUnderlyingNetworkChanged() won't be fired twice if network capabilities doesn't
         // change.
-        cb.onCapabilitiesChanged(mNetwork, SUSPENDED_NETWORK_CAPABILITIES);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
         verify(mNetworkControllerCb, times(1))
                 .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
@@ -425,19 +453,21 @@
         UnderlyingNetworkListener cb =
                 verifyRegistrationOnAvailableAndGetCallback(SUSPENDED_NETWORK_CAPABILITIES);
 
-        cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        INITIAL_NETWORK_CAPABILITIES,
+                        responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb, times(1))
                 .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
         // onSelectedUnderlyingNetworkChanged() won't be fired twice if network capabilities doesn't
         // change.
-        cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
         verify(mNetworkControllerCb, times(1))
                 .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
@@ -451,7 +481,7 @@
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        INITIAL_NETWORK_CAPABILITIES,
+                        buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS),
                         INITIAL_LINK_PROPERTIES,
                         true /* isBlocked */);
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
@@ -470,7 +500,8 @@
     public void testRecordTrackerCallbackIgnoresDuplicateRecord() {
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
-        cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
+        cb.onCapabilitiesChanged(
+                mNetwork, buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS));
 
         // Verify no more calls to the UnderlyingNetworkControllerCallback when the
         // UnderlyingNetworkRecord does not actually change
@@ -482,7 +513,8 @@
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
         mUnderlyingNetworkController.teardown();
 
-        cb.onCapabilitiesChanged(mNetwork, UPDATED_NETWORK_CAPABILITIES);
+        cb.onCapabilitiesChanged(
+                mNetwork, buildResponseNwCaps(UPDATED_NETWORK_CAPABILITIES, INITIAL_SUB_IDS));
 
         // Verify that the only call was during onAvailable()
         verify(mNetworkControllerCb, times(1)).onSelectedUnderlyingNetworkChanged(any());