Merge "Expire child sessions (3/n)"
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 a8a252e..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);
@@ -44678,6 +44688,7 @@
     field public static final int TYPE_TEXT_FLAG_CAP_CHARACTERS = 4096; // 0x1000
     field public static final int TYPE_TEXT_FLAG_CAP_SENTENCES = 16384; // 0x4000
     field public static final int TYPE_TEXT_FLAG_CAP_WORDS = 8192; // 0x2000
+    field public static final int TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS = 1048576; // 0x100000
     field public static final int TYPE_TEXT_FLAG_IME_MULTI_LINE = 262144; // 0x40000
     field public static final int TYPE_TEXT_FLAG_MULTI_LINE = 131072; // 0x20000
     field public static final int TYPE_TEXT_FLAG_NO_SUGGESTIONS = 524288; // 0x80000
@@ -45807,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);
@@ -51331,6 +51344,7 @@
     method public void addAccessibilityServicesStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener);
     method public boolean addAccessibilityStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener);
     method public void addAccessibilityStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener, @Nullable android.os.Handler);
+    method public void addAudioDescriptionByDefaultStateChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.accessibility.AccessibilityManager.AudioDescriptionByDefaultStateChangeListener);
     method public boolean addTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener);
     method public void addTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener, @Nullable android.os.Handler);
     method @ColorInt public int getAccessibilityFocusColor();
@@ -51347,6 +51361,7 @@
     method public void removeAccessibilityRequestPreparer(android.view.accessibility.AccessibilityRequestPreparer);
     method public boolean removeAccessibilityServicesStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener);
     method public boolean removeAccessibilityStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener);
+    method public boolean removeAudioDescriptionByDefaultStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AudioDescriptionByDefaultStateChangeListener);
     method public boolean removeTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener);
     method public void sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
     field public static final int FLAG_CONTENT_CONTROLS = 4; // 0x4
@@ -51362,6 +51377,10 @@
     method public void onAccessibilityStateChanged(boolean);
   }
 
+  public static interface AccessibilityManager.AudioDescriptionByDefaultStateChangeListener {
+    method public void onAudioDescriptionByDefaultStateChanged(boolean);
+  }
+
   public static interface AccessibilityManager.TouchExplorationStateChangeListener {
     method public void onTouchExplorationStateChanged(boolean);
   }
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 187b08b..a298354 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -75,6 +75,7 @@
     field public static final String BROADCAST_CLOSE_SYSTEM_DIALOGS = "android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS";
     field @Deprecated public static final String BROADCAST_NETWORK_PRIVILEGED = "android.permission.BROADCAST_NETWORK_PRIVILEGED";
     field public static final String BYPASS_ROLE_QUALIFICATION = "android.permission.BYPASS_ROLE_QUALIFICATION";
+    field public static final String CALL_AUDIO_INTERCEPTION = "android.permission.CALL_AUDIO_INTERCEPTION";
     field public static final String CAMERA_DISABLE_TRANSMIT_LED = "android.permission.CAMERA_DISABLE_TRANSMIT_LED";
     field public static final String CAMERA_OPEN_CLOSE_LISTENER = "android.permission.CAMERA_OPEN_CLOSE_LISTENER";
     field public static final String CAPTURE_AUDIO_HOTWORD = "android.permission.CAPTURE_AUDIO_HOTWORD";
@@ -5408,6 +5409,8 @@
     method @IntRange(from=0) public long getAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static java.util.List<android.media.audiopolicy.AudioProductStrategy> getAudioProductStrategies();
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static java.util.List<android.media.audiopolicy.AudioVolumeGroup> getAudioVolumeGroups();
+    method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat);
+    method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioTrack getCallUplinkInjectionAudioTrack(@NonNull android.media.AudioFormat);
     method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public int getDeviceVolumeBehavior(@NonNull android.media.AudioDeviceAttributes);
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public java.util.List<android.media.AudioDeviceAttributes> getDevicesForAttributes(@NonNull android.media.AudioAttributes);
     method @IntRange(from=0) public long getMaxAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo);
@@ -5420,6 +5423,7 @@
     method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
     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 int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
     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);
@@ -7585,7 +7589,7 @@
     method @NonNull public static android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings.Builder builder();
     method public int getCodeRate();
     method public int getModulation();
-    method @IntRange(from=0, to=255) public int getNumberOfSegment();
+    method @IntRange(from=0, to=255) public int getNumberOfSegments();
     method public int getTimeInterleaveMode();
   }
 
@@ -7593,7 +7597,7 @@
     method @NonNull public android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings build();
     method @NonNull public android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings.Builder setCodeRate(int);
     method @NonNull public android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings.Builder setModulation(int);
-    method @IntRange(from=0, to=255) @NonNull public android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings.Builder setNumberOfSegment(int);
+    method @IntRange(from=0, to=255) @NonNull public android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings.Builder setNumberOfSegments(int);
     method @NonNull public android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings.Builder setTimeInterleaveMode(int);
   }
 
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index f20f963..1014673 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1445,6 +1445,8 @@
 
   public class AudioManager {
     method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int abandonAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String);
+    method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat);
+    method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioTrack getCallUplinkInjectionAudioTrack(@NonNull android.media.AudioFormat);
     method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int);
     method @IntRange(from=0) @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFadeOutDurationOnFocusLossMillis(@NonNull android.media.AudioAttributes);
     method public static final int[] getPublicStreamTypes();
@@ -1453,6 +1455,7 @@
     method @NonNull public java.util.Map<java.lang.Integer,java.lang.Boolean> getSurroundFormats();
     method public boolean hasRegisteredDynamicPolicy();
     method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.QUERY_AUDIO_STATE}) public boolean isFullVolumeDevice();
+    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);
   }
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/Resources.java b/core/java/android/content/res/Resources.java
index a6f2e40..632d8e5 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -1923,6 +1923,25 @@
                 }
             }
         }
+
+        @Override
+        public int hashCode() {
+            return getKey().hashCode();
+        }
+
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (this == o) {
+                return true;
+            }
+
+            if (o == null || getClass() != o.getClass() || hashCode() != o.hashCode()) {
+                return false;
+            }
+
+            final Theme other = (Theme) o;
+            return getKey().equals(other.getKey());
+        }
     }
 
     static class ThemeKey implements Cloneable {
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/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 627e09e..adf7955c 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -274,6 +274,15 @@
     public static final int FLAG_STORAGE_EXTERNAL = IInstalld.FLAG_STORAGE_EXTERNAL;
 
     /** {@hide} */
+    @IntDef(prefix = "FLAG_STORAGE_",  value = {
+            FLAG_STORAGE_DE,
+            FLAG_STORAGE_CE,
+            FLAG_STORAGE_EXTERNAL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface StorageFlags {}
+
+    /** {@hide} */
     public static final int FLAG_FOR_WRITE = 1 << 8;
     /** {@hide} */
     public static final int FLAG_REAL_STATE = 1 << 9;
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index 5036abc..eef1ff7 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -2956,7 +2956,10 @@
          */
         @Nullable
         public static String getLocalAccountName(@NonNull Context context) {
-            return null;
+            //  config_rawContactsLocalAccountName is defined in
+            //  platform/frameworks/base/core/res/res/values/config.xml
+            return TextUtils.nullIfEmpty(context.getString(
+                    com.android.internal.R.string.config_rawContactsLocalAccountName));
         }
 
         /**
@@ -2972,7 +2975,10 @@
          */
         @Nullable
         public static String getLocalAccountType(@NonNull Context context) {
-            return null;
+            //  config_rawContactsLocalAccountType is defined in
+            //  platform/frameworks/base/core/res/res/values/config.xml
+            return TextUtils.nullIfEmpty(context.getString(
+                    com.android.internal.R.string.config_rawContactsLocalAccountType));
         }
 
         /**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 030f171..49a211f 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -965,6 +965,22 @@
     public static final String ACTION_LOCKSCREEN_SETTINGS = "android.settings.LOCK_SCREEN_SETTINGS";
 
     /**
+     * Activity Action: Show settings to allow pairing bluetooth devices.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you
+     * safeguard against this.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_BLUETOOTH_PAIRING_SETTINGS =
+            "android.settings.BLUETOOTH_PAIRING_SETTINGS";
+
+    /**
      * Activity Action: Show settings to configure input methods, in particular
      * allowing the user to enable input methods.
      * <p>
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/InputType.java b/core/java/android/text/InputType.java
index 7f903b6..dfe4119 100644
--- a/core/java/android/text/InputType.java
+++ b/core/java/android/text/InputType.java
@@ -16,6 +16,12 @@
 
 package android.text;
 
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.TextAttribute;
+import android.view.inputmethod.TextAttribute.TextAttributeBuilder;
+
+import java.util.List;
+
 /**
  * Bit definitions for an integer defining the basic content type of text
  * held in an {@link Editable} object. Supported classes may be combined
@@ -188,6 +194,21 @@
      */
     public static final int TYPE_TEXT_FLAG_NO_SUGGESTIONS = 0x00080000;
 
+    /**
+     * Flag for {@link #TYPE_CLASS_TEXT}: Let the IME know the text conversion suggestions are
+     * required by the application. Text conversion suggestion is for the transliteration languages
+     * which has pronunciation characters and target characters. When the user is typing the
+     * pronunciation charactes, the IME could provide the possible target characters to the user.
+     * When this flag is set, the IME should insert the text conversion suggestions through
+     * {@link TextAttributeBuilder#setTextConversionSuggestions(List)} and
+     * the {@link TextAttribute} with initialized with the text conversion suggestions is provided
+     * by the IME to the application. To receive the additional information, the application needs
+     * to implement {@link InputConnection#setComposingText(CharSequence, int, TextAttribute)},
+     * {@link InputConnection#setComposingRegion(int, int, TextAttribute)}, and
+     * {@link InputConnection#commitText(CharSequence, int, TextAttribute)}.
+     */
+    public static final int TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS = 0x00100000;
+
     // ----------------------------------------------------------------------
 
     /**
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/util/LocalLog.java b/core/java/android/util/LocalLog.java
index 8c4dcb3..cd077e1 100644
--- a/core/java/android/util/LocalLog.java
+++ b/core/java/android/util/LocalLog.java
@@ -22,6 +22,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.util.ArrayDeque;
@@ -63,7 +64,8 @@
         if (mUseLocalTimestamps) {
             logLine = LocalDateTime.now() + " - " + msg;
         } else {
-            logLine = SystemClock.elapsedRealtime() + " / " + Instant.now() + " - " + msg;
+            logLine = Duration.ofMillis(SystemClock.elapsedRealtime())
+                    + " / " + Instant.now() + " - " + msg;
         }
         append(logLine);
     }
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index dc61727..078b767 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -273,6 +273,9 @@
     private final ArrayMap<AccessibilityServicesStateChangeListener, Executor>
             mServicesStateChangeListeners = new ArrayMap<>();
 
+    private final ArrayMap<AudioDescriptionByDefaultStateChangeListener, Executor>
+            mAudioDescriptionByDefaultStateChangeListeners = new ArrayMap<>();
+
     /**
      * Map from a view's accessibility id to the list of request preparers set for that view
      */
@@ -353,6 +356,21 @@
     }
 
     /**
+     * Listener for the audio description by default state. To listen for
+     * changes to the audio description by default state on the device,
+     * implement this interface and register it with the system by calling
+     * {@link #addAudioDescriptionByDefaultStateChangeListener}.
+     */
+    public interface AudioDescriptionByDefaultStateChangeListener {
+        /**
+         * Called when the audio description enabled state changes.
+         *
+         * @param enabled Whether audio description by default is enabled.
+         */
+        void onAudioDescriptionByDefaultStateChanged(boolean enabled);
+    }
+
+    /**
      * Policy to inject behavior into the accessibility manager.
      *
      * @hide
@@ -1159,6 +1177,35 @@
     }
 
     /**
+     * Registers a {@link AudioDescriptionByDefaultStateChangeListener}
+     * for changes in the audio description by default state of the system.
+     * The value could be read via {@link #isAudioDescriptionRequested}.
+     *
+     * @param executor The executor on which the listener should be called back.
+     * @param listener The listener.
+     */
+    public void addAudioDescriptionByDefaultStateChangeListener(
+            @NonNull Executor executor,
+            @NonNull AudioDescriptionByDefaultStateChangeListener listener) {
+        synchronized (mLock) {
+            mAudioDescriptionByDefaultStateChangeListeners.put(listener, executor);
+        }
+    }
+
+    /**
+     * Unregisters a {@link AudioDescriptionByDefaultStateChangeListener}.
+     *
+     * @param listener The listener.
+     * @return True if listener was previously registered.
+     */
+    public boolean removeAudioDescriptionByDefaultStateChangeListener(
+            @NonNull AudioDescriptionByDefaultStateChangeListener listener) {
+        synchronized (mLock) {
+            return (mAudioDescriptionByDefaultStateChangeListeners.remove(listener) != null);
+        }
+    }
+
+    /**
      * Sets the {@link AccessibilityPolicy} controlling this manager.
      *
      * @param policy The policy.
@@ -1303,7 +1350,7 @@
         final boolean wasEnabled = isEnabled();
         final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
         final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;
-
+        final boolean wasAudioDescriptionByDefaultRequested = mIsAudioDescriptionByDefaultRequested;
 
         // Ensure listeners get current state from isZzzEnabled() calls.
         mIsEnabled = enabled;
@@ -1323,6 +1370,11 @@
             notifyHighTextContrastStateChanged();
         }
 
+        if (wasAudioDescriptionByDefaultRequested
+                != audioDescriptionEnabled) {
+            notifyAudioDescriptionbyDefaultStateChanged();
+        }
+
         updateAccessibilityTracingState(stateFlags);
     }
 
@@ -1688,15 +1740,20 @@
 
     /**
      * Determines if users want to select sound track with audio description by default.
-     *
+     * <p>
      * Audio description, also referred to as a video description, described video, or
      * more precisely called a visual description, is a form of narration used to provide
      * information surrounding key visual elements in a media work for the benefit of
      * blind and visually impaired consumers.
-     *
+     * </p>
+     * <p>
      * The method provides the preference value to content provider apps to select the
      * default sound track during playing a video or movie.
-     *
+     * </p>
+     * <p>
+     * Add listener to detect the state change via
+     * {@link #addAudioDescriptionByDefaultStateChangeListener}
+     * </p>
      * @return {@code true} if the audio description is enabled, {@code false} otherwise.
      */
     public boolean isAudioDescriptionRequested() {
@@ -1804,6 +1861,29 @@
     }
 
     /**
+     * Notifies the registered {@link AudioDescriptionStateChangeListener}s.
+     */
+    private void notifyAudioDescriptionbyDefaultStateChanged() {
+        final boolean isAudioDescriptionByDefaultRequested;
+        final ArrayMap<AudioDescriptionByDefaultStateChangeListener, Executor> listeners;
+        synchronized (mLock) {
+            if (mAudioDescriptionByDefaultStateChangeListeners.isEmpty()) {
+                return;
+            }
+            isAudioDescriptionByDefaultRequested = mIsAudioDescriptionByDefaultRequested;
+            listeners = new ArrayMap<>(mAudioDescriptionByDefaultStateChangeListeners);
+        }
+
+        final int numListeners = listeners.size();
+        for (int i = 0; i < numListeners; i++) {
+            final AudioDescriptionByDefaultStateChangeListener listener = listeners.keyAt(i);
+            listeners.valueAt(i).execute(() ->
+                    listener.onAudioDescriptionByDefaultStateChanged(
+                        isAudioDescriptionByDefaultRequested));
+        }
+    }
+
+    /**
      * Update mAccessibilityTracingState.
      */
     private void updateAccessibilityTracingState(int stateFlag) {
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index 74ca913..4cbd477 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -92,6 +92,7 @@
      *                1                  TYPE_TEXT_FLAG_MULTI_LINE
      *               1                   TYPE_TEXT_FLAG_IME_MULTI_LINE
      *              1                    TYPE_TEXT_FLAG_NO_SUGGESTIONS
+     *             1                     TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS
      * |-------|-------|-------|-------|
      *                                1  TYPE_CLASS_NUMBER
      *                             1     TYPE_NUMBER_VARIATION_PASSWORD
@@ -1090,4 +1091,4 @@
     public int describeContents() {
         return 0;
     }
-}
\ No newline at end of file
+}
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 424632fe..6541b14 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -270,8 +270,6 @@
     private Drawable mCaptionBackgroundDrawable;
     private Drawable mUserCaptionBackgroundDrawable;
 
-    private float mAvailableWidth;
-
     String mLogTag = TAG;
     private final Rect mFloatingInsets = new Rect();
     private boolean mApplyFloatingVerticalInsets = false;
@@ -315,8 +313,6 @@
         mSemiTransparentBarColor = context.getResources().getColor(
                 R.color.system_bar_background_semi_transparent, null /* theme */);
 
-        updateAvailableWidth();
-
         setWindow(window);
 
         updateLogTag(params);
@@ -697,7 +693,8 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+        final Resources res = getContext().getResources();
+        final DisplayMetrics metrics = res.getDisplayMetrics();
         final boolean isPortrait =
                 getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
 
@@ -767,17 +764,19 @@
 
         if (!fixedWidth && widthMode == AT_MOST) {
             final TypedValue tv = isPortrait ? mWindow.mMinWidthMinor : mWindow.mMinWidthMajor;
+            final float availableWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    res.getConfiguration().screenWidthDp, metrics);
             if (tv.type != TypedValue.TYPE_NULL) {
                 final int min;
                 if (tv.type == TypedValue.TYPE_DIMENSION) {
-                    min = (int)tv.getDimension(metrics);
+                    min = (int) tv.getDimension(metrics);
                 } else if (tv.type == TypedValue.TYPE_FRACTION) {
-                    min = (int)tv.getFraction(mAvailableWidth, mAvailableWidth);
+                    min = (int) tv.getFraction(availableWidth, availableWidth);
                 } else {
                     min = 0;
                 }
                 if (DEBUG_MEASURE) Log.d(mLogTag, "Adjust for min width: " + min + ", value::"
-                        + tv.coerceToString() + ", mAvailableWidth=" + mAvailableWidth);
+                        + tv.coerceToString() + ", mAvailableWidth=" + availableWidth);
 
                 if (width < min) {
                     widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY);
@@ -2144,7 +2143,6 @@
 
         updateDecorCaptionStatus(newConfig);
 
-        updateAvailableWidth();
         initializeElevation();
     }
 
@@ -2616,12 +2614,6 @@
         mLogTag = TAG + "[" + getTitleSuffix(params) + "]";
     }
 
-    private void updateAvailableWidth() {
-        Resources res = getResources();
-        mAvailableWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
-                res.getConfiguration().screenWidthDp, res.getDisplayMetrics());
-    }
-
     /**
      * @hide
      */
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/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4d2d200..04d6171 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4695,6 +4695,13 @@
     <permission android:name="android.permission.MODIFY_AUDIO_ROUTING"
         android:protectionLevel="signature|privileged|role" />
 
+    <!-- @SystemApi Allows an application to access the uplink and downlink audio of an ongoing
+        call.
+         <p>Not for use by third-party applications.</p>
+         @hide -->
+    <permission android:name="android.permission.CALL_AUDIO_INTERCEPTION"
+                android:protectionLevel="signature|privileged" />
+
     <!-- @TestApi Allows an application to query audio related state.
          @hide -->
     <permission android:name="android.permission.QUERY_AUDIO_STATE"
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 697ec20..6076645 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -1329,6 +1329,11 @@
              dictionary-based word suggestions.  Corresponds to
              {@link android.text.InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS}. -->
         <flag name="textNoSuggestions" value="0x00080001" />
+        <!-- Can be combined with <var>text</var> and its variations to
+             indicate that if there is extra information, the IME should provide
+             {@link android.view.inputmethod.TextAttribute}.  Corresponds to
+             {@link android.text.InputType#TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS}. -->
+        <flag name="textEnableTextConversionSuggestions" value="0x00100001" />
         <!-- Text that will be used as a URI.  Corresponds to
              {@link android.text.InputType#TYPE_CLASS_TEXT} |
              {@link android.text.InputType#TYPE_TEXT_VARIATION_URI}. -->
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 088d5b3..70a3fc7 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -495,6 +495,8 @@
         <!-- Permission required for CTS test - SystemMediaRouter2Test -->
         <permission name="android.permission.MEDIA_CONTENT_CONTROL"/>
         <permission name="android.permission.MODIFY_AUDIO_ROUTING"/>
+        <!-- Permission required for CTS test - CallAudioInterceptionTest -->
+        <permission name="android.permission.CALL_AUDIO_INTERCEPTION"/>
         <!-- Permission required for CTS test - CtsPermission5TestCases -->
         <permission name="android.permission.RENOUNCE_PERMISSIONS" />
         <permission name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS" />
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..dc4e27a 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);
+            }
         }
     }
 
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/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
index 0fbdf90..8467cc5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
@@ -120,6 +120,11 @@
         mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge);
         mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
 
+        if (mTargetViewContainer != null) {
+            // init can be called multiple times, remove the old one from view hierarchy first.
+            mWindowManager.removeViewImmediate(mTargetViewContainer);
+        }
+
         mTargetView = new DismissCircleView(mContext);
         mTargetViewContainer = new FrameLayout(mContext);
         mTargetViewContainer.setBackgroundDrawable(
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/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index 54252b5..9993ce9 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -481,13 +481,20 @@
      */
     public static final int FLAG_NEVER_SPATIALIZE = 0x1 << 15;
 
+    /**
+     * @hide
+     * Flag indicating the audio is part of a call redirection.
+     * Valid for playback and capture.
+     */
+    public static final int FLAG_CALL_REDIRECTION = 0x1 << 16;
+
     // Note that even though FLAG_MUTE_HAPTIC is stored as a flag bit, it is not here since
     // it is known as a boolean value outside of AudioAttributes.
     private static final int FLAG_ALL = FLAG_AUDIBILITY_ENFORCED | FLAG_SECURE | FLAG_SCO
             | FLAG_BEACON | FLAG_HW_AV_SYNC | FLAG_HW_HOTWORD | FLAG_BYPASS_INTERRUPTION_POLICY
             | FLAG_BYPASS_MUTE | FLAG_LOW_LATENCY | FLAG_DEEP_BUFFER | FLAG_NO_MEDIA_PROJECTION
             | FLAG_NO_SYSTEM_CAPTURE | FLAG_CAPTURE_PRIVATE | FLAG_CONTENT_SPATIALIZED
-            | FLAG_NEVER_SPATIALIZE;
+            | FLAG_NEVER_SPATIALIZE | FLAG_CALL_REDIRECTION;
     private final static int FLAG_ALL_PUBLIC = FLAG_AUDIBILITY_ENFORCED |
             FLAG_HW_AV_SYNC | FLAG_LOW_LATENCY;
     /* mask of flags that can be set by SDK and System APIs through the Builder */
@@ -707,6 +714,14 @@
         return ALLOW_CAPTURE_BY_ALL;
     }
 
+    /**
+     * @hide
+     * Indicates if the audio is used for call redirection
+     * @return true if used for call redirection, false otherwise.
+     */
+    public boolean isForCallRedirection() {
+        return (mFlags & FLAG_CALL_REDIRECTION) == FLAG_CALL_REDIRECTION;
+    }
 
     /**
      * Builder class for {@link AudioAttributes} objects.
@@ -763,11 +778,15 @@
         public Builder(AudioAttributes aa) {
             mUsage = aa.mUsage;
             mContentType = aa.mContentType;
+            mSource = aa.mSource;
             mFlags = aa.getAllFlags();
             mTags = (HashSet<String>) aa.mTags.clone();
             mMuteHapticChannels = aa.areHapticChannelsMuted();
             mIsContentSpatialized = aa.isContentSpatialized();
             mSpatializationBehavior = aa.getSpatializationBehavior();
+            if ((mFlags & FLAG_CAPTURE_PRIVATE) != 0) {
+                mPrivacySensitive = PRIVACY_SENSITIVE_ENABLED;
+            }
         }
 
         /**
@@ -1071,6 +1090,17 @@
         }
 
         /**
+         * @hide
+         * Replace all custom tags
+         * @param tags
+         * @return the same Builder instance.
+         */
+        public Builder replaceTags(HashSet<String> tags) {
+            mTags = (HashSet<String>) tags.clone();
+            return this;
+        }
+
+        /**
          * Sets attributes as inferred from the legacy stream types.
          * Warning: do not use this method in combination with setting any other attributes such as
          * usage, content type, flags or haptic control, as this method will overwrite (the more
@@ -1245,6 +1275,16 @@
                 privacySensitive ? PRIVACY_SENSITIVE_ENABLED : PRIVACY_SENSITIVE_DISABLED;
             return this;
         }
+
+        /**
+         * @hide
+         * Designates the audio to be used for call redirection
+         * @return the same Builder instance.
+         */
+        public Builder setForCallRedirection() {
+            mFlags |= FLAG_CALL_REDIRECTION;
+            return this;
+        }
     };
 
     @Override
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index faae9a5..46aad3f 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -88,7 +88,7 @@
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
-
+import java.util.concurrent.Executors;
 
 /**
  * AudioManager provides access to volume and ringer mode control.
@@ -7570,7 +7570,7 @@
         return getDeviceInfoFromTypeAndAddress(deviceType, null);
     }
 
-        /**
+    /**
      * @hide
      * Returns an {@link AudioDeviceInfo} corresponding to a connected device of the type and
      * address provided.
@@ -7692,6 +7692,281 @@
         }
     }
 
+
+    /**
+     * @hide
+     * Indicates if the platform allows accessing the uplink and downlink audio of an ongoing
+     * PSTN call.
+     * When true, {@link getCallUplinkInjectionAudioTrack(AudioFormat)} can be used to obtain
+     * an AudioTrack for call uplink audio injection and
+     * {@link getCallDownlinkExtractionAudioRecord(AudioFormat)} can be used to obtain
+     * an AudioRecord for call downlink audio extraction.
+     * @return true if PSTN call audio is accessible, false otherwise.
+     */
+    @TestApi
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION)
+    public boolean isPstnCallAudioInterceptable() {
+        final IAudioService service = getService();
+        try {
+            return service.isPstnCallAudioInterceptable();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    @IntDef(flag = false, prefix = "CALL_REDIRECT_", value = {
+            CALL_REDIRECT_NONE,
+            CALL_REDIRECT_PSTN,
+            CALL_REDIRECT_VOIP }
+            )
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CallRedirectionMode {}
+
+    /**
+     * Not used for call redirection
+     * @hide
+     */
+    public static final int CALL_REDIRECT_NONE = 0;
+    /**
+     * Used to redirect  PSTN call
+     * @hide
+     */
+    public static final int CALL_REDIRECT_PSTN = 1;
+    /**
+     * Used to redirect  VoIP call
+     * @hide
+     */
+    public static final int CALL_REDIRECT_VOIP = 2;
+
+
+    private @CallRedirectionMode int getCallRedirectMode() {
+        int mode = getMode();
+        if (mode == MODE_IN_CALL || mode == MODE_CALL_SCREENING
+                || mode == MODE_CALL_REDIRECT) {
+            return CALL_REDIRECT_PSTN;
+        } else if (mode == MODE_IN_COMMUNICATION || mode == MODE_COMMUNICATION_REDIRECT) {
+            return CALL_REDIRECT_VOIP;
+        }
+        return CALL_REDIRECT_NONE;
+    }
+
+    private void checkCallRedirectionFormat(AudioFormat format, boolean isOutput) {
+        if (format.getEncoding() != AudioFormat.ENCODING_PCM_16BIT
+                && format.getEncoding() != AudioFormat.ENCODING_PCM_FLOAT) {
+            throw new UnsupportedOperationException(" Unsupported encoding ");
+        }
+        if (format.getSampleRate() < 8000
+                || format.getSampleRate() > 48000) {
+            throw new UnsupportedOperationException(" Unsupported sample rate ");
+        }
+        if (isOutput && format.getChannelMask() != AudioFormat.CHANNEL_OUT_MONO
+                && format.getChannelMask() != AudioFormat.CHANNEL_OUT_STEREO) {
+            throw new UnsupportedOperationException(" Unsupported output channel mask ");
+        }
+        if (!isOutput && format.getChannelMask() != AudioFormat.CHANNEL_IN_MONO
+                && format.getChannelMask() != AudioFormat.CHANNEL_IN_STEREO) {
+            throw new UnsupportedOperationException(" Unsupported input channel mask ");
+        }
+    }
+
+    class CallIRedirectionClientInfo {
+        public WeakReference trackOrRecord;
+        public int redirectMode;
+    }
+
+    private Object mCallRedirectionLock = new Object();
+    @GuardedBy("mCallRedirectionLock")
+    private CallInjectionModeChangedListener mCallRedirectionModeListener;
+    @GuardedBy("mCallRedirectionLock")
+    private ArrayList<CallIRedirectionClientInfo> mCallIRedirectionClients;
+
+    /**
+     * @hide
+     * Returns an AudioTrack that can be used to inject audio to an active call uplink.
+     * This can be used for functions like call screening or call audio redirection and is reserved
+     * to system apps with privileged permission.
+     * @param format the desired audio format for audio playback.
+     * p>Formats accepted are:
+     * <ul>
+     *   <li><em>Sampling rate</em> - 8kHz to 48kHz. </li>
+     *   <li><em>Channel mask</em> - Mono or Stereo </li>
+     *   <li><em>Sample format</em> - PCM 16 bit or FLOAT 32 bit </li>
+     * </ul>
+     *
+     * @return The AudioTrack used for audio injection
+     * @throws NullPointerException if AudioFormat argument is null.
+     * @throws UnsupportedOperationException if on unsupported AudioFormat is specified.
+     * @throws IllegalArgumentException if an invalid AudioFormat is specified.
+     * @throws SecurityException if permission CALL_AUDIO_INTERCEPTION  is missing .
+     * @throws IllegalStateException if current audio mode is not MODE_IN_CALL,
+     *         MODE_IN_COMMUNICATION, MODE_CALL_SCREENING, MODE_CALL_REDIRECT
+     *         or MODE_COMMUNICATION_REDIRECT.
+     */
+    @TestApi
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION)
+    public @NonNull AudioTrack getCallUplinkInjectionAudioTrack(@NonNull AudioFormat format) {
+        Objects.requireNonNull(format);
+        checkCallRedirectionFormat(format, true /* isOutput */);
+
+        AudioTrack track = null;
+        int redirectMode = getCallRedirectMode();
+        if (redirectMode == CALL_REDIRECT_NONE) {
+            throw new IllegalStateException(
+                    " not available in mode " + AudioSystem.modeToString(getMode()));
+        } else if (redirectMode == CALL_REDIRECT_PSTN && !isPstnCallAudioInterceptable()) {
+            throw new UnsupportedOperationException(" PSTN Call audio not accessible ");
+        }
+
+        track = new AudioTrack.Builder()
+                .setAudioAttributes(new AudioAttributes.Builder()
+                        .setSystemUsage(AudioAttributes.USAGE_CALL_ASSISTANT)
+                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+                        .build())
+                .setAudioFormat(format)
+                .setCallRedirectionMode(redirectMode)
+                .build();
+
+        if (track != null && track.getState() != AudioTrack.STATE_UNINITIALIZED) {
+            synchronized (mCallRedirectionLock) {
+                if (mCallRedirectionModeListener == null) {
+                    mCallRedirectionModeListener = new CallInjectionModeChangedListener();
+                    try {
+                        addOnModeChangedListener(
+                                Executors.newSingleThreadExecutor(), mCallRedirectionModeListener);
+                    } catch (Exception e) {
+                        Log.e(TAG, "addOnModeChangedListener failed with exception: " + e);
+                        mCallRedirectionModeListener = null;
+                        throw new UnsupportedOperationException(" Cannot register mode listener ");
+                    }
+                    mCallIRedirectionClients = new ArrayList<CallIRedirectionClientInfo>();
+                }
+                CallIRedirectionClientInfo info = new CallIRedirectionClientInfo();
+                info.redirectMode = redirectMode;
+                info.trackOrRecord = new WeakReference<AudioTrack>(track);
+                mCallIRedirectionClients.add(info);
+            }
+        } else {
+            throw new UnsupportedOperationException(" Cannot create the AudioTrack");
+        }
+        return track;
+    }
+
+    /**
+     * @hide
+     * Returns an AudioRecord that can be used to extract audio from an active call downlink.
+     * This can be used for functions like call screening or call audio redirection and is reserved
+     * to system apps with privileged permission.
+     * @param format the desired audio format for audio capture.
+     *<p>Formats accepted are:
+     * <ul>
+     *   <li><em>Sampling rate</em> - 8kHz to 48kHz. </li>
+     *   <li><em>Channel mask</em> - Mono or Stereo </li>
+     *   <li><em>Sample format</em> - PCM 16 bit or FLOAT 32 bit </li>
+     * </ul>
+     *
+     * @return The AudioRecord used for audio extraction
+     * @throws UnsupportedOperationException if on unsupported AudioFormat is specified.
+     * @throws IllegalArgumentException if an invalid AudioFormat is specified.
+     * @throws NullPointerException if AudioFormat argument is null.
+     * @throws SecurityException if permission CALL_AUDIO_INTERCEPTION  is missing .
+     * @throws IllegalStateException if current audio mode is not MODE_IN_CALL,
+     *         MODE_IN_COMMUNICATION, MODE_CALL_SCREENING, MODE_CALL_REDIRECT
+     *         or MODE_COMMUNICATION_REDIRECT.
+     */
+    @TestApi
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION)
+    public @NonNull AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull AudioFormat format) {
+        Objects.requireNonNull(format);
+        checkCallRedirectionFormat(format, false /* isOutput */);
+
+        AudioRecord record = null;
+        int redirectMode = getCallRedirectMode();
+        if (redirectMode == CALL_REDIRECT_NONE) {
+            throw new IllegalStateException(
+                    " not available in mode " + AudioSystem.modeToString(getMode()));
+        } else if (redirectMode == CALL_REDIRECT_PSTN && !isPstnCallAudioInterceptable()) {
+            throw new UnsupportedOperationException(" PSTN Call audio not accessible ");
+        }
+
+        record = new AudioRecord.Builder()
+                .setAudioAttributes(new AudioAttributes.Builder()
+                        .setInternalCapturePreset(MediaRecorder.AudioSource.VOICE_DOWNLINK)
+                        .build())
+                .setAudioFormat(format)
+                .setCallRedirectionMode(redirectMode)
+                .build();
+
+        if (record != null && record.getState() != AudioRecord.STATE_UNINITIALIZED) {
+            synchronized (mCallRedirectionLock) {
+                if (mCallRedirectionModeListener == null) {
+                    mCallRedirectionModeListener = new CallInjectionModeChangedListener();
+                    try {
+                        addOnModeChangedListener(
+                                Executors.newSingleThreadExecutor(), mCallRedirectionModeListener);
+                    } catch (Exception e) {
+                        Log.e(TAG, "addOnModeChangedListener failed with exception: " + e);
+                        mCallRedirectionModeListener = null;
+                        throw new UnsupportedOperationException(" Cannot register mode listener ");
+                    }
+                    mCallIRedirectionClients = new ArrayList<CallIRedirectionClientInfo>();
+                }
+                CallIRedirectionClientInfo info = new CallIRedirectionClientInfo();
+                info.redirectMode = redirectMode;
+                info.trackOrRecord = new WeakReference<AudioRecord>(record);
+                mCallIRedirectionClients.add(info);
+            }
+        } else {
+            throw new UnsupportedOperationException(" Cannot create the AudioRecord");
+        }
+        return record;
+    }
+
+    class CallInjectionModeChangedListener implements OnModeChangedListener {
+        @Override
+        public void onModeChanged(@AudioMode int mode) {
+            synchronized (mCallRedirectionLock) {
+                final ArrayList<CallIRedirectionClientInfo> clientInfos =
+                        (ArrayList<CallIRedirectionClientInfo>) mCallIRedirectionClients.clone();
+                for (CallIRedirectionClientInfo info : clientInfos) {
+                    Object trackOrRecord = info.trackOrRecord.get();
+                    if (trackOrRecord != null) {
+                        if ((info.redirectMode ==  CALL_REDIRECT_PSTN
+                                && mode != MODE_IN_CALL && mode != MODE_CALL_SCREENING
+                                && mode != MODE_CALL_REDIRECT)
+                                || (info.redirectMode == CALL_REDIRECT_VOIP
+                                    && mode != MODE_IN_COMMUNICATION
+                                    && mode != MODE_COMMUNICATION_REDIRECT)) {
+                            if (trackOrRecord instanceof AudioTrack) {
+                                AudioTrack track = (AudioTrack) trackOrRecord;
+                                track.release();
+                            } else {
+                                AudioRecord record = (AudioRecord) trackOrRecord;
+                                record.release();
+                            }
+                            mCallIRedirectionClients.remove(info);
+                        }
+                    }
+                }
+                if (mCallIRedirectionClients.isEmpty()) {
+                    try {
+                        if (mCallRedirectionModeListener != null) {
+                            removeOnModeChangedListener(mCallRedirectionModeListener);
+                        }
+                    } catch (Exception e) {
+                        Log.e(TAG, "removeOnModeChangedListener failed with exception: " + e);
+                    } finally {
+                        mCallRedirectionModeListener = null;
+                        mCallIRedirectionClients = null;
+                    }
+                }
+            }
+        }
+    }
+
     //---------------------------------------------------------
     // Inner classes
     //--------------------
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 7c6ae28..e76bb42 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -32,6 +32,7 @@
 import android.content.Context;
 import android.media.MediaRecorder.Source;
 import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
 import android.media.audiopolicy.AudioPolicy;
 import android.media.metrics.LogSessionId;
 import android.media.projection.MediaProjection;
@@ -58,6 +59,7 @@
 import java.lang.ref.WeakReference;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
@@ -404,7 +406,9 @@
 
         // is this AudioRecord using REMOTE_SUBMIX at full volume?
         if (attributes.getCapturePreset() == MediaRecorder.AudioSource.REMOTE_SUBMIX) {
-            final AudioAttributes.Builder filteredAttr = new AudioAttributes.Builder();
+            final AudioAttributes.Builder ab =
+                    new AudioAttributes.Builder(attributes);
+            HashSet<String> filteredTags = new HashSet<String>();
             final Iterator<String> tagsIter = attributes.getTags().iterator();
             while (tagsIter.hasNext()) {
                 final String tag = tagsIter.next();
@@ -412,15 +416,15 @@
                     mIsSubmixFullVolume = true;
                     Log.v(TAG, "Will record from REMOTE_SUBMIX at full fixed volume");
                 } else { // SUBMIX_FIXED_VOLUME: is not to be propagated to the native layers
-                    filteredAttr.addTag(tag);
+                    filteredTags.add(tag);
                 }
             }
-            filteredAttr.setInternalCapturePreset(attributes.getCapturePreset());
-            mAudioAttributes = filteredAttr.build();
-        } else {
-            mAudioAttributes = attributes;
+            ab.replaceTags(filteredTags);
+            attributes = ab.build();
         }
 
+        mAudioAttributes = attributes;
+
         int rate = format.getSampleRate();
         if (rate == AudioFormat.SAMPLE_RATE_UNSPECIFIED) {
             rate = 0;
@@ -432,7 +436,7 @@
             encoding = format.getEncoding();
         }
 
-        audioParamCheck(attributes.getCapturePreset(), rate, encoding);
+        audioParamCheck(mAudioAttributes.getCapturePreset(), rate, encoding);
 
         if ((format.getPropertySetMask()
                 & AudioFormat.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_INDEX_MASK) != 0) {
@@ -595,6 +599,8 @@
         private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
         private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT;
         private int mMaxSharedAudioHistoryMs = 0;
+        private int mCallRedirectionMode = AudioManager.CALL_REDIRECT_NONE;
+
         private static final int PRIVACY_SENSITIVE_DEFAULT = -1;
         private static final int PRIVACY_SENSITIVE_DISABLED = 0;
         private static final int PRIVACY_SENSITIVE_ENABLED = 1;
@@ -791,6 +797,65 @@
 
         /**
          * @hide
+         * Sets the {@link AudioRecord} call redirection mode.
+         * Used when creating an AudioRecord to extract audio from call downlink path. The mode
+         * indicates if the call is a PSTN call or a VoIP call in which case a dynamic audio
+         * policy is created to forward all playback with voice communication usage this record.
+         *
+         * @param callRedirectionMode one of
+         * {@link AudioManager#CALL_REDIRECT_NONE},
+         * {@link AudioManager#CALL_REDIRECT_PSTN},
+         * or {@link AAudioManager#CALL_REDIRECT_VOIP}.
+         * @return the same Builder instance.
+         * @throws IllegalArgumentException if {@code callRedirectionMode} is not valid.
+         */
+        public @NonNull Builder setCallRedirectionMode(
+                @AudioManager.CallRedirectionMode int callRedirectionMode) {
+            switch (callRedirectionMode) {
+                case AudioManager.CALL_REDIRECT_NONE:
+                case AudioManager.CALL_REDIRECT_PSTN:
+                case AudioManager.CALL_REDIRECT_VOIP:
+                    mCallRedirectionMode = callRedirectionMode;
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Invalid call redirection mode " + callRedirectionMode);
+            }
+            return this;
+        }
+
+        private @NonNull AudioRecord buildCallExtractionRecord() {
+            AudioMixingRule audioMixingRule = new AudioMixingRule.Builder()
+                    .addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE,
+                            new AudioAttributes.Builder()
+                                .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+                                .setForCallRedirection()
+                                .build())
+                    .addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE,
+                            new AudioAttributes.Builder()
+                                .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
+                                .setForCallRedirection()
+                                .build())
+                    .setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS)
+                    .build();
+            AudioMix audioMix = new AudioMix.Builder(audioMixingRule)
+                    .setFormat(mFormat)
+                    .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
+                    .build();
+            AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build();
+            if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
+                throw new UnsupportedOperationException("Error: could not register audio policy");
+            }
+            AudioRecord record = audioPolicy.createAudioRecordSink(audioMix);
+            if (record == null) {
+                throw new UnsupportedOperationException("Cannot create extraction AudioRecord");
+            }
+            record.unregisterAudioPolicyOnRelease(audioPolicy);
+            return record;
+        }
+
+        /**
+         * @hide
          * Specifies the maximum duration in the past of the this AudioRecord's capture buffer
          * that can be shared with another app by calling
          * {@link AudioRecord#shareAudioHistory(String, long)}.
@@ -897,6 +962,14 @@
                         .build();
             }
 
+            if (mCallRedirectionMode == AudioManager.CALL_REDIRECT_VOIP) {
+                return buildCallExtractionRecord();
+            } else if (mCallRedirectionMode == AudioManager.CALL_REDIRECT_PSTN) {
+                mAttributes = new AudioAttributes.Builder(mAttributes)
+                        .setForCallRedirection()
+                        .build();
+            }
+
             try {
                 // If the buffer size is not specified,
                 // use a single frame for the buffer size and let the
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 0450a80..ea692b9 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -26,6 +26,9 @@
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+import android.media.audiopolicy.AudioPolicy;
 import android.media.metrics.LogSessionId;
 import android.os.Binder;
 import android.os.Build;
@@ -574,6 +577,8 @@
      */
     @NonNull private LogSessionId mLogSessionId = LogSessionId.LOG_SESSION_ID_NONE;
 
+    private AudioPolicy mAudioPolicy;
+
     //--------------------------------
     // Used exclusively by native code
     //--------------------
@@ -1032,6 +1037,7 @@
         private int mPerformanceMode = PERFORMANCE_MODE_NONE;
         private boolean mOffload = false;
         private TunerConfiguration mTunerConfiguration;
+        private int mCallRedirectionMode = AudioManager.CALL_REDIRECT_NONE;
 
         /**
          * Constructs a new Builder with the default values as described above.
@@ -1227,6 +1233,74 @@
         }
 
         /**
+         * Sets the tuner configuration for the {@code AudioTrack}.
+         *
+         * The {@link AudioTrack.TunerConfiguration} consists of parameters obtained from
+         * the Android TV tuner API which indicate the audio content stream id and the
+         * synchronization id for the {@code AudioTrack}.
+         *
+         * @param tunerConfiguration obtained by {@link AudioTrack.TunerConfiguration.Builder}.
+         * @return the same Builder instance.
+         * @hide
+         */
+
+        /**
+         * @hide
+         * Sets the {@link AudioTrack} call redirection mode.
+         * Used when creating an AudioTrack to inject audio to call uplink path. The mode
+         * indicates if the call is a PSTN call or a VoIP call in which case a dynamic audio
+         * policy is created to use this track as the source for all capture with voice
+         * communication preset.
+         *
+         * @param callRedirectionMode one of
+         * {@link AudioManager#CALL_REDIRECT_NONE},
+         * {@link AudioManager#CALL_REDIRECT_PSTN},
+         * or {@link AAudioManager#CALL_REDIRECT_VOIP}.
+         * @return the same Builder instance.
+         * @throws IllegalArgumentException if {@code callRedirectionMode} is not valid.
+         */
+        public @NonNull Builder setCallRedirectionMode(
+                @AudioManager.CallRedirectionMode int callRedirectionMode) {
+            switch (callRedirectionMode) {
+                case AudioManager.CALL_REDIRECT_NONE:
+                case AudioManager.CALL_REDIRECT_PSTN:
+                case AudioManager.CALL_REDIRECT_VOIP:
+                    mCallRedirectionMode = callRedirectionMode;
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Invalid call redirection mode " + callRedirectionMode);
+            }
+            return this;
+        }
+
+        private @NonNull AudioTrack buildCallInjectionTrack() {
+            AudioMixingRule audioMixingRule = new AudioMixingRule.Builder()
+                    .addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET,
+                            new AudioAttributes.Builder()
+                                   .setCapturePreset(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
+                                   .setForCallRedirection()
+                                   .build())
+                    .setTargetMixRole(AudioMixingRule.MIX_ROLE_INJECTOR)
+                    .build();
+            AudioMix audioMix = new AudioMix.Builder(audioMixingRule)
+                    .setFormat(mFormat)
+                    .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
+                    .build();
+            AudioPolicy audioPolicy =
+                    new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build();
+            if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
+                throw new UnsupportedOperationException("Error: could not register audio policy");
+            }
+            AudioTrack track = audioPolicy.createAudioTrackSource(audioMix);
+            if (track == null) {
+                throw new UnsupportedOperationException("Cannot create injection AudioTrack");
+            }
+            track.unregisterAudioPolicyOnRelease(audioPolicy);
+            return track;
+        }
+
+        /**
          * Builds an {@link AudioTrack} instance initialized with all the parameters set
          * on this <code>Builder</code>.
          * @return a new successfully initialized {@link AudioTrack} instance.
@@ -1270,6 +1344,14 @@
                         .build();
             }
 
+            if (mCallRedirectionMode == AudioManager.CALL_REDIRECT_VOIP) {
+                return buildCallInjectionTrack();
+            } else if (mCallRedirectionMode == AudioManager.CALL_REDIRECT_PSTN) {
+                mAttributes = new AudioAttributes.Builder(mAttributes)
+                        .setForCallRedirection()
+                        .build();
+            }
+
             if (mOffload) {
                 if (mPerformanceMode == PERFORMANCE_MODE_LOW_LATENCY) {
                     throw new UnsupportedOperationException(
@@ -1315,6 +1397,16 @@
     }
 
     /**
+     * Sets an {@link AudioPolicy} to automatically unregister when the track is released.
+     *
+     * <p>This is to prevent users of the call audio injection API from having to manually
+     * unregister the policy that was used to create the track.
+     */
+    private void unregisterAudioPolicyOnRelease(AudioPolicy audioPolicy) {
+        mAudioPolicy = audioPolicy;
+    }
+
+    /**
      * Configures the delay and padding values for the current compressed stream playing
      * in offload mode.
      * This can only be used on a track successfully initialized with
@@ -1879,6 +1971,11 @@
         } catch(IllegalStateException ise) {
             // don't raise an exception, we're releasing the resources.
         }
+        if (mAudioPolicy != null) {
+            AudioManager.unregisterAudioPolicyAsyncStatic(mAudioPolicy);
+            mAudioPolicy = null;
+        }
+
         baseRelease();
         native_release();
         synchronized (mPlayStateLock) {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 35191bd..f15f880 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -445,4 +445,6 @@
     void unregisterSpatializerOutputCallback(in ISpatializerOutputCallback cb);
 
     boolean isVolumeFixed();
+
+    boolean isPstnCallAudioInterceptable();
 }
diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index 8080aad..f85bdee 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -241,6 +241,11 @@
     }
 
     /** @hide */
+    public boolean isForCallRedirection() {
+        return mRule.isForCallRedirection();
+    }
+
+    /** @hide */
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java
index 6112290..c912759 100644
--- a/media/java/android/media/audiopolicy/AudioMixingRule.java
+++ b/media/java/android/media/audiopolicy/AudioMixingRule.java
@@ -23,6 +23,7 @@
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.media.AudioAttributes;
+import android.media.MediaRecorder;
 import android.os.Build;
 import android.os.Parcel;
 import android.util.Log;
@@ -262,6 +263,22 @@
     }
 
     /** @hide */
+    public boolean isForCallRedirection() {
+        for (AudioMixMatchCriterion criterion : mCriteria) {
+            if (criterion.mAttr != null
+                    && (criterion.mRule == RULE_MATCH_ATTRIBUTE_USAGE
+                        && criterion.mAttr.getUsage() == AudioAttributes.USAGE_VOICE_COMMUNICATION)
+                    || (criterion.mRule == RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET
+                        && criterion.mAttr.getCapturePreset()
+                            == MediaRecorder.AudioSource.VOICE_COMMUNICATION)
+                    && criterion.mAttr.isForCallRedirection()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** @hide */
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 3e8d76a..0f08d79 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -588,6 +588,10 @@
         boolean canModifyAudioRouting = PackageManager.PERMISSION_GRANTED
                 == checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING);
 
+        boolean canInterceptCallAudio = PackageManager.PERMISSION_GRANTED
+                == checkCallingOrSelfPermission(
+                        android.Manifest.permission.CALL_AUDIO_INTERCEPTION);
+
         boolean canProjectAudio;
         try {
             canProjectAudio = mProjection != null && mProjection.getProjection().canProjectAudio();
@@ -596,7 +600,9 @@
             throw e.rethrowFromSystemServer();
         }
 
-        if (!((isLoopbackRenderPolicy() && canProjectAudio) || canModifyAudioRouting)) {
+        if (!((isLoopbackRenderPolicy() && canProjectAudio)
+                || (isCallRedirectionPolicy() && canInterceptCallAudio)
+                || canModifyAudioRouting)) {
             Slog.w(TAG, "Cannot use AudioPolicy for pid " + Binder.getCallingPid() + " / uid "
                     + Binder.getCallingUid() + ", needs MODIFY_AUDIO_ROUTING or "
                     + "MediaProjection that can project audio.");
@@ -612,6 +618,17 @@
         }
     }
 
+    private boolean isCallRedirectionPolicy() {
+        synchronized (mLock) {
+            for (AudioMix mix : mConfig.mMixes) {
+                if (mix.isForCallRedirection()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
     /**
      * Returns {@link PackageManager#PERMISSION_GRANTED} if the caller has the given permission.
      */
@@ -728,13 +745,16 @@
                 .setChannelMask(AudioFormat.inChannelMaskFromOutChannelMask(
                         mix.getFormat().getChannelMask()))
                 .build();
+
+        AudioAttributes.Builder ab = new AudioAttributes.Builder()
+                .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX)
+                .addTag(addressForTag(mix))
+                .addTag(AudioRecord.SUBMIX_FIXED_VOLUME);
+        if (mix.isForCallRedirection()) {
+            ab.setForCallRedirection();
+        }
         // create the AudioRecord, configured for loop back, using the same format as the mix
-        AudioRecord ar = new AudioRecord(
-                new AudioAttributes.Builder()
-                        .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX)
-                        .addTag(addressForTag(mix))
-                        .addTag(AudioRecord.SUBMIX_FIXED_VOLUME)
-                        .build(),
+        AudioRecord ar = new AudioRecord(ab.build(),
                 mixFormat,
                 AudioRecord.getMinBufferSize(mix.getFormat().getSampleRate(),
                         // using stereo for buffer size to avoid the current poor support for masks
@@ -768,11 +788,13 @@
         }
         checkMixReadyToUse(mix, true/*for an AudioTrack*/);
         // create the AudioTrack, configured for loop back, using the same format as the mix
-        AudioTrack at = new AudioTrack(
-                new AudioAttributes.Builder()
-                        .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE)
-                        .addTag(addressForTag(mix))
-                        .build(),
+        AudioAttributes.Builder ab = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE)
+                .addTag(addressForTag(mix));
+        if (mix.isForCallRedirection()) {
+            ab.setForCallRedirection();
+        }
+        AudioTrack at = new AudioTrack(ab.build(),
                 mix.getFormat(),
                 AudioTrack.getMinBufferSize(mix.getFormat().getSampleRate(),
                         mix.getFormat().getChannelMask(), mix.getFormat().getEncoding()),
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/tuner/frontend/IsdbtFrontendSettings.java b/media/java/android/media/tv/tuner/frontend/IsdbtFrontendSettings.java
index 403bfa7..89512a0 100644
--- a/media/java/android/media/tv/tuner/frontend/IsdbtFrontendSettings.java
+++ b/media/java/android/media/tv/tuner/frontend/IsdbtFrontendSettings.java
@@ -546,14 +546,14 @@
         private final int mModulation;
         private final int mTimeInterleaveMode;
         private final int mCodeRate;
-        private final int mNumOfSegment;
+        private final int mNumOfSegments;
 
         private IsdbtLayerSettings(
-                int modulation, int timeInterleaveMode, int codeRate, int numOfSegment) {
+                int modulation, int timeInterleaveMode, int codeRate, int numOfSegments) {
             mModulation = modulation;
             mTimeInterleaveMode = timeInterleaveMode;
             mCodeRate = codeRate;
-            mNumOfSegment = numOfSegment;
+            mNumOfSegments = numOfSegments;
         }
 
         /**
@@ -578,11 +578,11 @@
             return mCodeRate;
         }
         /**
-         * Gets Number of Segment.
+         * Gets Number of Segments.
          */
         @IntRange(from = 0, to = 0xff)
-        public int getNumberOfSegment() {
-            return mNumOfSegment;
+        public int getNumberOfSegments() {
+            return mNumOfSegments;
         }
 
         /**
@@ -600,7 +600,7 @@
             private int mModulation = MODULATION_UNDEFINED;
             private int mTimeInterleaveMode = TIME_INTERLEAVE_MODE_UNDEFINED;
             private int mCodeRate = DvbtFrontendSettings.CODERATE_UNDEFINED;
-            private int mNumOfSegment = 0;
+            private int mNumOfSegments = 0;
 
             private Builder() {}
 
@@ -633,14 +633,14 @@
                 return this;
             }
             /**
-             * Sets number of segment.
+             * Sets number of segments.
              *
              * <p>Default value is 0.
              */
             @NonNull
             @IntRange(from = 0, to = 0xff)
-            public Builder setNumberOfSegment(int numOfSegment) {
-                mNumOfSegment = numOfSegment;
+            public Builder setNumberOfSegments(int numOfSegments) {
+                mNumOfSegments = numOfSegments;
                 return this;
             }
 
@@ -650,7 +650,7 @@
             @NonNull
             public IsdbtLayerSettings build() {
                 return new IsdbtLayerSettings(
-                        mModulation, mTimeInterleaveMode, mCodeRate, mNumOfSegment);
+                        mModulation, mTimeInterleaveMode, mCodeRate, mNumOfSegments);
             }
         }
     }
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/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 4c1ed45..ded9652 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -2961,7 +2961,7 @@
         frontendIsdbtSettings.layerSettings[i].coderate = static_cast<FrontendIsdbtCoderate>(
                 env->GetIntField(layer, env->GetFieldID(layerClazz, "mCodeRate", "I")));
         frontendIsdbtSettings.layerSettings[i].numOfSegment =
-                env->GetIntField(layer, env->GetFieldID(layerClazz, "mNumOfSegment", "I"));
+                env->GetIntField(layer, env->GetFieldID(layerClazz, "mNumOfSegments", "I"));
     }
 
     frontendSettings.set<FrontendSettings::Tag::isdbt>(frontendIsdbtSettings);
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/Nsd/service/src/com/android/server/NativeDaemonConnector.java b/packages/Nsd/service/src/com/android/server/NativeDaemonConnector.java
index 02475dd..ec8d779 100644
--- a/packages/Nsd/service/src/com/android/server/NativeDaemonConnector.java
+++ b/packages/Nsd/service/src/com/android/server/NativeDaemonConnector.java
@@ -20,16 +20,15 @@
 import android.net.LocalSocketAddress;
 import android.os.Build;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.util.LocalLog;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.power.ShutdownThread;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -83,13 +82,6 @@
 
     NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
             int responseQueueSize, String logTag, int maxLogSize, PowerManager.WakeLock wl) {
-        this(callbacks, socket, responseQueueSize, logTag, maxLogSize, wl,
-                FgThread.get().getLooper());
-    }
-
-    NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
-            int responseQueueSize, String logTag, int maxLogSize, PowerManager.WakeLock wl,
-            Looper looper) {
         mCallbacks = callbacks;
         mSocket = socket;
         mResponseQueue = new ResponseQueue(responseQueueSize);
@@ -97,10 +89,12 @@
         if (mWakeLock != null) {
             mWakeLock.setReferenceCounted(true);
         }
-        mLooper = looper;
         mSequenceNumber = new AtomicInteger(0);
         TAG = logTag != null ? logTag : "NativeDaemonConnector";
         mLocalLog = new LocalLog(maxLogSize);
+        final HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
+        mLooper = thread.getLooper();
     }
 
     /**
@@ -135,23 +129,15 @@
         mCallbackHandler = new Handler(mLooper, this);
 
         while (true) {
-            if (isShuttingDown()) break;
             try {
                 listenToSocket();
             } catch (Exception e) {
                 loge("Error in NativeDaemonConnector: " + e);
-                if (isShuttingDown()) break;
                 SystemClock.sleep(5000);
             }
         }
     }
 
-    private static boolean isShuttingDown() {
-        String shutdownAct = SystemProperties.get(
-            ShutdownThread.SHUTDOWN_ACTION_PROPERTY, "");
-        return shutdownAct != null && shutdownAct.length() > 0;
-    }
-
     @Override
     public boolean handleMessage(Message msg) {
         final String event = (String) msg.obj;
diff --git a/packages/Nsd/service/src/com/android/server/NsdService.java b/packages/Nsd/service/src/com/android/server/NsdService.java
index 76ecea7..497107d 100644
--- a/packages/Nsd/service/src/com/android/server/NsdService.java
+++ b/packages/Nsd/service/src/com/android/server/NsdService.java
@@ -16,12 +16,9 @@
 
 package com.android.server;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.database.ContentObserver;
-import android.net.Uri;
 import android.net.nsd.INsdManager;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
@@ -33,7 +30,6 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.util.Base64;
 import android.util.Log;
 import android.util.Pair;
@@ -66,7 +62,6 @@
     private static final long CLEANUP_DELAY_MS = 10000;
 
     private final Context mContext;
-    private final NsdSettings mNsdSettings;
     private final NsdStateMachine mNsdStateMachine;
     private final DaemonConnection mDaemon;
     private final NativeCallbackReceiver mDaemonCallback;
@@ -121,30 +116,14 @@
             this.removeMessages(NsdManager.DAEMON_CLEANUP);
         }
 
-        /**
-         * Observes the NSD on/off setting, and takes action when changed.
-         */
-        private void registerForNsdSetting() {
-            final ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
-                @Override
-                public void onChange(boolean selfChange) {
-                    notifyEnabled(isNsdEnabled());
-                }
-            };
-
-            final Uri uri = Settings.Global.getUriFor(Settings.Global.NSD_ON);
-            mNsdSettings.registerContentObserver(uri, contentObserver);
-        }
-
         NsdStateMachine(String name, Handler handler) {
             super(name, handler);
             addState(mDefaultState);
                 addState(mDisabledState, mDefaultState);
                 addState(mEnabledState, mDefaultState);
-            State initialState = isNsdEnabled() ? mEnabledState : mDisabledState;
+            State initialState = mEnabledState;
             setInitialState(initialState);
             setLogRecSize(25);
-            registerForNsdSetting();
         }
 
         class DefaultState extends State {
@@ -580,11 +559,9 @@
     }
 
     @VisibleForTesting
-    NsdService(Context ctx, NsdSettings settings, Handler handler,
-            DaemonConnectionSupplier fn, long cleanupDelayMs) {
+    NsdService(Context ctx, Handler handler, DaemonConnectionSupplier fn, long cleanupDelayMs) {
         mCleanupDelayMs = cleanupDelayMs;
         mContext = ctx;
-        mNsdSettings = settings;
         mNsdStateMachine = new NsdStateMachine(TAG, handler);
         mNsdStateMachine.start();
         mDaemonCallback = new NativeCallbackReceiver();
@@ -592,12 +569,11 @@
     }
 
     public static NsdService create(Context context) throws InterruptedException {
-        NsdSettings settings = NsdSettings.makeDefault(context);
         HandlerThread thread = new HandlerThread(TAG);
         thread.start();
         Handler handler = new Handler(thread.getLooper());
-        NsdService service = new NsdService(context, settings, handler,
-                DaemonConnection::new, CLEANUP_DELAY_MS);
+        NsdService service =
+                new NsdService(context, handler, DaemonConnection::new, CLEANUP_DELAY_MS);
         service.mDaemonCallback.awaitConnection();
         return service;
     }
@@ -669,10 +645,6 @@
         }
     }
 
-    private void notifyEnabled(boolean isEnabled) {
-        mNsdStateMachine.sendMessage(isEnabled ? NsdManager.ENABLE : NsdManager.DISABLE);
-    }
-
     private void sendNsdStateChangeBroadcast(boolean isEnabled) {
         final Intent intent = new Intent(NsdManager.ACTION_NSD_STATE_CHANGED);
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
@@ -681,14 +653,6 @@
         mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
     }
 
-    private boolean isNsdEnabled() {
-        boolean ret = mNsdSettings.isEnabled();
-        if (DBG) {
-            Log.d(TAG, "Network service discovery is " + (ret ? "enabled" : "disabled"));
-        }
-        return ret;
-    }
-
     private int getUniqueId() {
         if (++mUniqueId == INVALID_ID) return ++mUniqueId;
         return mUniqueId;
@@ -1075,35 +1039,4 @@
             }
         }
     }
-
-    /**
-     * Interface which encapsulates dependencies of NsdService that are hard to mock, hard to
-     * override, or have side effects on global state in unit tests.
-     */
-    @VisibleForTesting
-    public interface NsdSettings {
-        boolean isEnabled();
-        void putEnabledStatus(boolean isEnabled);
-        void registerContentObserver(Uri uri, ContentObserver observer);
-
-        static NsdSettings makeDefault(Context context) {
-            final ContentResolver resolver = context.getContentResolver();
-            return new NsdSettings() {
-                @Override
-                public boolean isEnabled() {
-                    return Settings.Global.getInt(resolver, Settings.Global.NSD_ON, 1) == 1;
-                }
-
-                @Override
-                public void putEnabledStatus(boolean isEnabled) {
-                    Settings.Global.putInt(resolver, Settings.Global.NSD_ON, isEnabled ? 1 : 0);
-                }
-
-                @Override
-                public void registerContentObserver(Uri uri, ContentObserver observer) {
-                    resolver.registerContentObserver(uri, false, observer);
-                }
-            };
-        }
-    }
 }
diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-v31/dimens.xml b/packages/SettingsLib/MainSwitchPreference/res/values-v31/dimens.xml
index 2272a37..2624a41 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/values-v31/dimens.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/values-v31/dimens.xml
@@ -21,10 +21,10 @@
     <dimen name="settingslib_switchbar_margin">16dp</dimen>
 
     <!-- Size of layout margin left -->
-    <dimen name="settingslib_switchbar_padding_left">24dp</dimen>
+    <dimen name="settingslib_switchbar_padding_left">20dp</dimen>
 
     <!-- Size of layout margin right -->
-    <dimen name="settingslib_switchbar_padding_right">16dp</dimen>
+    <dimen name="settingslib_switchbar_padding_right">20dp</dimen>
 
     <!-- Minimum width of switch -->
     <dimen name="settingslib_min_switch_width">52dp</dimen>
diff --git a/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml b/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml
index 6362882..157a54e 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml
@@ -24,7 +24,7 @@
     <dimen name="settingslib_restricted_icon_margin_end">16dp</dimen>
 
     <!-- Size of title margin -->
-    <dimen name="settingslib_switch_title_margin">16dp</dimen>
+    <dimen name="settingslib_switch_title_margin">24dp</dimen>
 
     <!-- SwitchBar sub settings margin start / end -->
     <dimen name="settingslib_switchbar_subsettings_margin_start">72dp</dimen>
diff --git a/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java b/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java
index ff00fb3..0f34023 100644
--- a/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java
@@ -26,6 +26,7 @@
 import android.system.StructUtsname;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.BidiFormatter;
 import android.text.TextDirectionHeuristics;
@@ -33,7 +34,9 @@
 import android.text.format.DateFormat;
 import android.util.Log;
 
+import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 
 import java.io.BufferedReader;
 import java.io.FileReader;
@@ -179,10 +182,8 @@
             SubscriptionInfo subscriptionInfo) {
         String formattedNumber = null;
         if (subscriptionInfo != null) {
-            final TelephonyManager telephonyManager = context.getSystemService(
-                    TelephonyManager.class);
-            final String rawNumber = telephonyManager.createForSubscriptionId(
-                    subscriptionInfo.getSubscriptionId()).getLine1Number();
+            final String rawNumber = getRawPhoneNumber(
+                    context, subscriptionInfo.getSubscriptionId());
             if (!TextUtils.isEmpty(rawNumber)) {
                 formattedNumber = PhoneNumberUtils.formatNumber(rawNumber);
             }
@@ -194,12 +195,10 @@
             List<SubscriptionInfo> subscriptionInfoList) {
         StringBuilder sb = new StringBuilder();
         if (subscriptionInfoList != null) {
-            final TelephonyManager telephonyManager = context.getSystemService(
-                    TelephonyManager.class);
             final int count = subscriptionInfoList.size();
             for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
-                final String rawNumber = telephonyManager.createForSubscriptionId(
-                        subscriptionInfo.getSubscriptionId()).getLine1Number();
+                final String rawNumber = getRawPhoneNumber(
+                        context, subscriptionInfo.getSubscriptionId());
                 if (!TextUtils.isEmpty(rawNumber)) {
                     sb.append(PhoneNumberUtils.formatNumber(rawNumber)).append("\n");
                 }
@@ -219,4 +218,21 @@
         final String phoneNumber = getFormattedPhoneNumber(context, subscriptionInfo);
         return BidiFormatter.getInstance().unicodeWrap(phoneNumber, TextDirectionHeuristics.LTR);
     }
+
+    private static String getRawPhoneNumber(Context context, int subscriptionId) {
+        if (BuildCompat.isAtLeastT()) {
+            return getRawPhoneNumberFromT(context, subscriptionId);
+        } else {
+            final TelephonyManager telephonyManager = context.getSystemService(
+                    TelephonyManager.class);
+            return telephonyManager.createForSubscriptionId(subscriptionId).getLine1Number();
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private static String getRawPhoneNumberFromT(Context context, int subscriptionId) {
+        final SubscriptionManager subscriptionManager = context.getSystemService(
+                    SubscriptionManager.class);
+        return subscriptionManager.getPhoneNumber(subscriptionId);
+    }
 }
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 c5c3dd7..36b633b 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -571,6 +571,9 @@
     <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
 
+    <!-- Permission required for CTS test - CallAudioInterceptionTest -->
+    <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION" />
+
     <!-- Permission required for CTS test - CtsRotationResolverServiceDeviceTestCases -->
     <uses-permission android:name="android.permission.MANAGE_ROTATION_RESOLVER" />
 
diff --git a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml b/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml
deleted file mode 100644
index 620dd48..0000000
--- a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2021 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:pathData="M 0, 0 C 0.1217, 0.0462, 0.15, 0.4686, 0.1667, 0.66 C 0.1834, 0.8878, 0.1667, 1, 1, 1" />
\ No newline at end of file
diff --git a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml b/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml
deleted file mode 100644
index a268abc..0000000
--- a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2021 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1" />
\ No newline at end of file
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index fb80f1c..a0d335d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -21,6 +21,7 @@
 import android.app.PendingIntent
 import android.app.TaskInfo
 import android.graphics.Matrix
+import android.graphics.Path
 import android.graphics.Rect
 import android.graphics.RectF
 import android.os.Looper
@@ -34,6 +35,7 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.view.animation.Interpolator
 import android.view.animation.PathInterpolator
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
@@ -45,16 +47,46 @@
  * A class that allows activities to be started in a seamless way from a view that is transforming
  * nicely into the starting window.
  */
-class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) {
+class ActivityLaunchAnimator(
+    private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS)
+) {
     companion object {
+        @JvmField
+        val TIMINGS = LaunchAnimator.Timings(
+            totalDuration = 500L,
+            contentBeforeFadeOutDelay = 0L,
+            contentBeforeFadeOutDuration = 150L,
+            contentAfterFadeInDelay = 150L,
+            contentAfterFadeInDuration = 183L
+        )
+
+        val INTERPOLATORS = LaunchAnimator.Interpolators(
+            positionInterpolator = Interpolators.EMPHASIZED,
+            positionXInterpolator = createPositionXInterpolator(),
+            contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
+            contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f)
+        )
+
+        /** Durations & interpolators for the navigation bar fading in & out. */
         private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
         private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
-        private const val ANIMATION_DELAY_NAV_FADE_IN =
-            LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
+        private val ANIMATION_DELAY_NAV_FADE_IN =
+            TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN
+
+        private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
+        private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
+
+        /** The time we wait before timing out the remote animation after starting the intent. */
         private const val LAUNCH_TIMEOUT = 1000L
 
-        private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
-        private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
+        private fun createPositionXInterpolator(): Interpolator {
+            val path = Path().apply {
+                moveTo(0f, 0f)
+                cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f)
+                cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f)
+            }
+            return PathInterpolator(path)
+        }
     }
 
     /**
@@ -107,8 +139,8 @@
         val animationAdapter = if (!hideKeyguardWithAnimation) {
             RemoteAnimationAdapter(
                 runner,
-                LaunchAnimator.ANIMATION_DURATION,
-                LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
+                TIMINGS.totalDuration,
+                TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */
             )
         } else {
             null
@@ -448,7 +480,7 @@
             state: LaunchAnimator.State,
             linearProgress: Float
         ) {
-            val fadeInProgress = LaunchAnimator.getProgress(linearProgress,
+            val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
                 ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
 
             val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
@@ -463,7 +495,7 @@
                     .withWindowCrop(windowCrop)
                     .withVisibility(true)
             } else {
-                val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0,
+                val fadeOutProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 0,
                     ANIMATION_DURATION_NAV_FADE_OUT)
                 params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
             }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index de82ebd..066e169 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -20,7 +20,6 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.app.Dialog
-import android.content.Context
 import android.graphics.Color
 import android.graphics.Rect
 import android.os.Looper
@@ -28,10 +27,11 @@
 import android.util.Log
 import android.util.MathUtils
 import android.view.GhostView
+import android.view.SurfaceControl
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import android.view.ViewTreeObserver.OnPreDrawListener
+import android.view.ViewRootImpl
 import android.view.WindowManager
 import android.widget.FrameLayout
 import kotlin.math.roundToInt
@@ -42,12 +42,20 @@
  * A class that allows dialogs to be started in a seamless way from a view that is transforming
  * nicely into the starting dialog.
  */
-class DialogLaunchAnimator(
-    private val context: Context,
-    private val launchAnimator: LaunchAnimator,
-    private val dreamManager: IDreamManager
+class DialogLaunchAnimator @JvmOverloads constructor(
+    private val dreamManager: IDreamManager,
+    private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
+    private var isForTesting: Boolean = false
 ) {
     private companion object {
+        private val TIMINGS = ActivityLaunchAnimator.TIMINGS
+
+        // We use the same interpolator for X and Y axis to make sure the dialog does not move out
+        // of the screen bounds during the animation.
+        private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS.copy(
+            positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
+        )
+
         private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
     }
 
@@ -96,14 +104,14 @@
         animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
 
         val animatedDialog = AnimatedDialog(
-                context,
                 launchAnimator,
                 dreamManager,
                 animateFrom,
                 onDialogDismissed = { openedDialogs.remove(it) },
                 dialog = dialog,
                 animateBackgroundBoundsChange,
-                animatedParent
+                animatedParent,
+                isForTesting
         )
 
         openedDialogs.add(animatedDialog)
@@ -157,7 +165,6 @@
 }
 
 private class AnimatedDialog(
-    private val context: Context,
     private val launchAnimator: LaunchAnimator,
     private val dreamManager: IDreamManager,
 
@@ -174,10 +181,16 @@
     val dialog: Dialog,
 
     /** Whether we should animate the dialog background when its bounds change. */
-    private val animateBackgroundBoundsChange: Boolean,
+    animateBackgroundBoundsChange: Boolean,
 
     /** Launch animation corresponding to the parent [AnimatedDialog]. */
-    private val parentAnimatedDialog: AnimatedDialog? = null
+    private val parentAnimatedDialog: AnimatedDialog? = null,
+
+    /**
+     * Whether we are currently running in a test, in which case we need to disable
+     * synchronization.
+     */
+    private val isForTesting: Boolean
 ) {
     /**
      * The DecorView of this dialog window.
@@ -266,14 +279,14 @@
             // and the view that we added so that we can dismiss the dialog when this view is
             // clicked. This is necessary because DecorView overrides onTouchEvent and therefore we
             // can't set the click listener directly on the (now fullscreen) DecorView.
-            val fullscreenTransparentBackground = FrameLayout(context)
+            val fullscreenTransparentBackground = FrameLayout(dialog.context)
             decorView.addView(
                 fullscreenTransparentBackground,
                 0 /* index */,
                 FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
             )
 
-            val dialogContentWithBackground = FrameLayout(context)
+            val dialogContentWithBackground = FrameLayout(dialog.context)
             dialogContentWithBackground.background = decorView.background
 
             // Make the window background transparent. Note that setting the window (or DecorView)
@@ -365,59 +378,77 @@
         // Show the dialog.
         dialog.show()
 
-        // Add a temporary touch surface ghost as soon as the window is ready to draw. This
-        // temporary ghost will be drawn together with the touch surface, but in the dialog
-        // window. Once it is drawn, we will make the touch surface invisible, and then start the
-        // animation. We do all this synchronization to avoid flicker that would occur if we made
-        // the touch surface invisible too early (before its ghost is drawn), leading to one or more
-        // frames with a hole instead of the touch surface (or its ghost).
-        decorView.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
-            override fun onPreDraw(): Boolean {
-                decorView.viewTreeObserver.removeOnPreDrawListener(this)
-                addTemporaryTouchSurfaceGhost()
-                return true
-            }
-        })
-        decorView.invalidate()
+        addTouchSurfaceGhost()
     }
 
-    private fun addTemporaryTouchSurfaceGhost() {
+    private fun addTouchSurfaceGhost() {
+        if (decorView.viewRootImpl == null) {
+            // Make sure that we have access to the dialog view root to synchronize the creation of
+            // the ghost.
+            decorView.post(::addTouchSurfaceGhost)
+            return
+        }
+
         // Create a ghost of the touch surface (which will make the touch surface invisible) and add
-        // it to the dialog. We will wait for this ghost to be drawn before starting the animation.
-        val ghost = GhostView.addGhost(touchSurface, decorView)
-
-        // The ghost of the touch surface was just created, so the touch surface was made invisible.
-        // We make it visible again until the ghost is actually drawn.
-        touchSurface.visibility = View.VISIBLE
-
-        // Wait for the ghost to be drawn before continuing.
-        ghost.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
-            override fun onPreDraw(): Boolean {
-                ghost.viewTreeObserver.removeOnPreDrawListener(this)
-                onTouchSurfaceGhostDrawn()
-                return true
-            }
+        // it to the host dialog. We trigger a one off synchronization to make sure that this is
+        // done in sync between the two different windows.
+        synchronizeNextDraw(then = {
+            isTouchSurfaceGhostDrawn = true
+            maybeStartLaunchAnimation()
         })
-        ghost.invalidate()
+        GhostView.addGhost(touchSurface, decorView)
+
+        // The ghost of the touch surface was just created, so the touch surface is currently
+        // invisible. We need to make sure that it stays invisible as long as the dialog is shown or
+        // animating.
+        (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
     }
 
-    private fun onTouchSurfaceGhostDrawn() {
-        // Make the touch surface invisible and make sure that it stays invisible as long as the
-        // dialog is shown or animating.
-        touchSurface.visibility = View.INVISIBLE
-        (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+    /**
+     * Synchronize the next draw of the touch surface and dialog view roots so that they are
+     * performed at the same time, in the same transaction. This is necessary to make sure that the
+     * ghost of the touch surface is drawn at the same time as the touch surface is made invisible
+     * (or inversely, removed from the UI when the touch surface is made visible).
+     */
+    private fun synchronizeNextDraw(then: () -> Unit) {
+        if (isForTesting || !touchSurface.isAttachedToWindow || touchSurface.viewRootImpl == null ||
+            !decorView.isAttachedToWindow || decorView.viewRootImpl == null) {
+            // No need to synchronize if either the touch surface or dialog view is not attached
+            // to a window.
+            then()
+            return
+        }
 
-        // Add a pre draw listener to (maybe) start the animation once the touch surface is
-        // actually invisible.
-        touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
-            override fun onPreDraw(): Boolean {
-                touchSurface.viewTreeObserver.removeOnPreDrawListener(this)
-                isTouchSurfaceGhostDrawn = true
-                maybeStartLaunchAnimation()
-                return true
+        // Consume the next frames of both view roots to make sure the ghost view is drawn at
+        // exactly the same time as when the touch surface is made invisible.
+        var remainingTransactions = 0
+        val mergedTransactions = SurfaceControl.Transaction()
+
+        fun onTransaction(transaction: SurfaceControl.Transaction?) {
+            remainingTransactions--
+            transaction?.let { mergedTransactions.merge(it) }
+
+            if (remainingTransactions == 0) {
+                mergedTransactions.apply()
+                then()
             }
-        })
-        touchSurface.invalidate()
+        }
+
+        fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
+            if (viewRootImpl.consumeNextDraw(::onTransaction)) {
+                remainingTransactions++
+
+                // Make sure we trigger a traversal.
+                viewRootImpl.view.invalidate()
+            }
+        }
+
+        consumeNextDraw(touchSurface.viewRootImpl)
+        consumeNextDraw(decorView.viewRootImpl)
+
+        if (remainingTransactions == 0) {
+            then()
+        }
     }
 
     private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
@@ -483,7 +514,7 @@
 
     private fun onDialogDismissed() {
         if (Looper.myLooper() != Looper.getMainLooper()) {
-            context.mainExecutor.execute { onDialogDismissed() }
+            dialog.context.mainExecutor.execute { onDialogDismissed() }
             return
         }
 
@@ -556,25 +587,12 @@
                         .removeOnLayoutChangeListener(backgroundLayoutListener)
                 }
 
-                // The animated ghost was just removed. We create a temporary ghost that will be
-                // removed only once we draw the touch surface, to avoid flickering that would
-                // happen when removing the ghost too early (before the touch surface is drawn).
-                GhostView.addGhost(touchSurface, decorView)
-
-                touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
-                    override fun onPreDraw(): Boolean {
-                        touchSurface.viewTreeObserver.removeOnPreDrawListener(this)
-
-                        // Now that the touch surface was drawn, we can remove the temporary ghost
-                        // and instantly dismiss the dialog.
-                        GhostView.removeGhost(touchSurface)
-                        onAnimationFinished(true /* instantDismiss */)
-                        onDialogDismissed(this@AnimatedDialog)
-
-                        return true
-                    }
+                // Make sure that the removal of the ghost and making the touch surface visible is
+                // done at the same time.
+                synchronizeNextDraw(then = {
+                    onAnimationFinished(true /* instantDismiss */)
+                    onDialogDismissed(this@AnimatedDialog)
                 })
-                touchSurface.invalidate()
             }
         )
     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
index 3bf6c5e..ebe96eb 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
@@ -27,25 +27,19 @@
 import android.util.MathUtils
 import android.view.View
 import android.view.ViewGroup
-import android.view.animation.AnimationUtils
-import android.view.animation.PathInterpolator
+import android.view.animation.Interpolator
+import com.android.systemui.animation.Interpolators.LINEAR
 import kotlin.math.roundToInt
 
 private const val TAG = "LaunchAnimator"
 
 /** A base class to animate a window launch (activity or dialog) from a view . */
-class LaunchAnimator @JvmOverloads constructor(
-    context: Context,
-    private val isForTesting: Boolean = false
+class LaunchAnimator(
+    private val timings: Timings,
+    private val interpolators: Interpolators
 ) {
     companion object {
         internal const val DEBUG = false
-        const val ANIMATION_DURATION = 500L
-        private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
-        private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
-        private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
-
-        private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f)
         private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
 
         /**
@@ -53,23 +47,20 @@
          * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
          */
         @JvmStatic
-        fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
+        fun getProgress(
+            timings: Timings,
+            linearProgress: Float,
+            delay: Long,
+            duration: Long
+        ): Float {
             return MathUtils.constrain(
-                (linearProgress * ANIMATION_DURATION - delay) / duration,
+                (linearProgress * timings.totalDuration - delay) / duration,
                 0.0f,
                 1.0f
             )
         }
     }
 
-    /** The interpolator used for the width, height, Y position and corner radius. */
-    private val animationInterpolator = AnimationUtils.loadInterpolator(context,
-        R.interpolator.launch_animation_interpolator_y)
-
-    /** The interpolator used for the X position. */
-    private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
-        R.interpolator.launch_animation_interpolator_x)
-
     private val launchContainerLocation = IntArray(2)
     private val cornerRadii = FloatArray(8)
 
@@ -159,6 +150,45 @@
         fun cancel()
     }
 
+    /** The timings (durations and delays) used by this animator. */
+    class Timings(
+        /** The total duration of the animation. */
+        val totalDuration: Long,
+
+        /** The time to wait before fading out the expanding content. */
+        val contentBeforeFadeOutDelay: Long,
+
+        /** The duration of the expanding content fade out. */
+        val contentBeforeFadeOutDuration: Long,
+
+        /**
+         * The time to wait before fading in the expanded content (usually an activity or dialog
+         * window).
+         */
+        val contentAfterFadeInDelay: Long,
+
+        /** The duration of the expanded content fade in. */
+        val contentAfterFadeInDuration: Long
+    )
+
+    /** The interpolators used by this animator. */
+    data class Interpolators(
+        /** The interpolator used for the Y position, width, height and corner radius. */
+        val positionInterpolator: Interpolator,
+
+        /**
+         * The interpolator used for the X position. This can be different than
+         * [positionInterpolator] to create an arc-path during the animation.
+         */
+        val positionXInterpolator: Interpolator = positionInterpolator,
+
+        /** The interpolator used when fading out the expanding content. */
+        val contentBeforeFadeOutInterpolator: Interpolator,
+
+        /** The interpolator used when fading in the expanded content. */
+        val contentAfterFadeInInterpolator: Interpolator
+    )
+
     /**
      * Start a launch animation controlled by [controller] towards [endState]. An intermediary
      * layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and
@@ -221,8 +251,8 @@
 
         // Update state.
         val animator = ValueAnimator.ofFloat(0f, 1f)
-        animator.duration = if (isForTesting) 0 else ANIMATION_DURATION
-        animator.interpolator = Interpolators.LINEAR
+        animator.duration = timings.totalDuration
+        animator.interpolator = LINEAR
 
         val launchContainerOverlay = launchContainer.overlay
         var cancelled = false
@@ -260,8 +290,8 @@
             // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non
             // reversed animation.
             val linearProgress = animation.animatedFraction
-            val progress = animationInterpolator.getInterpolation(linearProgress)
-            val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
+            val progress = interpolators.positionInterpolator.getInterpolation(linearProgress)
+            val xProgress = interpolators.positionXInterpolator.getInterpolation(linearProgress)
 
             val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress)
             val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f
@@ -278,7 +308,12 @@
 
             // The expanding view can/should be hidden once it is completely covered by the opening
             // window.
-            state.visible = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
+            state.visible = getProgress(
+                timings,
+                linearProgress,
+                timings.contentBeforeFadeOutDelay,
+                timings.contentBeforeFadeOutDuration
+            ) < 1
 
             applyStateToWindowBackgroundLayer(
                 windowBackgroundLayer,
@@ -337,14 +372,25 @@
 
         // We first fade in the background layer to hide the expanding view, then fade it out
         // with SRC mode to draw a hole punch in the status bar and reveal the opening window.
-        val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
+        val fadeInProgress = getProgress(
+            timings,
+            linearProgress,
+            timings.contentBeforeFadeOutDelay,
+            timings.contentBeforeFadeOutDuration
+        )
         if (fadeInProgress < 1) {
-            val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress)
+            val alpha =
+                interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
             drawable.alpha = (alpha * 0xFF).roundToInt()
         } else {
             val fadeOutProgress = getProgress(
-                linearProgress, ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
-            val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
+                timings,
+                linearProgress,
+                timings.contentAfterFadeInDelay,
+                timings.contentAfterFadeInDuration
+            )
+            val alpha =
+                1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress)
             drawable.alpha = (alpha * 0xFF).roundToInt()
 
             if (drawHole) {
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
index e9bd638..e80cfaf 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
@@ -25,6 +25,9 @@
     <!-- Margin around the various security views -->
     <dimen name="keyguard_security_view_top_margin">12dp</dimen>
 
+    <!-- Padding for the lock icon on the keyguard -->
+    <dimen name="lock_icon_padding">16dp</dimen>
+
     <!-- Overload default clock widget parameters -->
     <dimen name="widget_big_font_size">100dp</dimen>
     <dimen name="widget_label_font_size">18sp</dimen>
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/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 012eda7..56317c6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.media.dialog;
 
+import static android.provider.Settings.ACTION_BLUETOOTH_PAIRING_SETTINGS;
+
 import android.app.Notification;
 import android.content.Context;
 import android.content.Intent;
@@ -35,6 +37,7 @@
 import android.media.session.PlaybackState;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
@@ -51,7 +54,6 @@
 import com.android.settingslib.media.InfoMediaManager;
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
-import com.android.settingslib.media.MediaOutputConstants;
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.R;
 import com.android.systemui.animation.DialogLaunchAnimator;
@@ -74,7 +76,8 @@
 
     private static final String TAG = "MediaOutputController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
+    private static final String PAGE_CONNECTED_DEVICES_KEY =
+            "top_level_connected_devices";
     private final String mPackageName;
     private final Context mContext;
     private final MediaSessionManager mMediaSessionManager;
@@ -485,14 +488,24 @@
         mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
 
         mCallback.dismissDialog();
-        final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
-            mContext.sendBroadcast(new Intent()
-                    .setAction(MediaOutputConstants.ACTION_LAUNCH_BLUETOOTH_PAIRING)
-                    .setPackage(MediaOutputConstants.SETTINGS_PACKAGE_NAME));
-            mShadeController.animateCollapsePanels();
-            return true;
-        };
-        mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
+        Intent launchIntent =
+                new Intent(ACTION_BLUETOOTH_PAIRING_SETTINGS)
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        final Intent deepLinkIntent =
+                new Intent(Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY);
+        if (deepLinkIntent.resolveActivity(mContext.getPackageManager()) != null) {
+            Log.d(TAG, "Device support split mode, launch page with deep link");
+            deepLinkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            deepLinkIntent.putExtra(
+                    Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI,
+                    launchIntent.toUri(Intent.URI_INTENT_SCHEME));
+            deepLinkIntent.putExtra(
+                    Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY,
+                    PAGE_CONNECTED_DEVICES_KEY);
+            mActivityStarter.startActivity(deepLinkIntent, true);
+            return;
+        }
+        mActivityStarter.startActivity(launchIntent, true);
     }
 
     void launchMediaOutputGroupDialog(View mediaOutputDialog) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index eddc206..d470fa2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -219,9 +219,8 @@
     }
 
     private void addTile(final QSTile tile, boolean collapsedView) {
-        final TileRecord r = new TileRecord();
-        r.tile = tile;
-        r.tileView = mHost.createTileView(getContext(), tile, collapsedView);
+        final TileRecord r =
+                new TileRecord(tile, mHost.createTileView(getContext(), tile, collapsedView));
         mView.addTile(r);
         mRecords.add(r);
         mCachedSpecs = getTilesSpecs();
@@ -418,6 +417,11 @@
 
     /** */
     public static final class TileRecord extends QSPanel.Record {
+        public TileRecord(QSTile tile, com.android.systemui.plugins.qs.QSTileView tileView) {
+            this.tile = tile;
+            this.tileView = tileView;
+        }
+
         public QSTile tile;
         public com.android.systemui.plugins.qs.QSTileView tileView;
         public boolean scanState;
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..65fe93c 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;
@@ -549,9 +549,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());
     }
 
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..bbbb764 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
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/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index 4c7ee17..8e2f88c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -25,7 +25,6 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
-import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -316,24 +315,15 @@
      */
     @Provides
     @SysUISingleton
-    static LaunchAnimator provideLaunchAnimator(Context context) {
-        return new LaunchAnimator(context);
+    static ActivityLaunchAnimator provideActivityLaunchAnimator() {
+        return new ActivityLaunchAnimator();
     }
 
     /**
      */
     @Provides
     @SysUISingleton
-    static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) {
-        return new ActivityLaunchAnimator(launchAnimator);
-    }
-
-    /**
-     */
-    @Provides
-    @SysUISingleton
-    static DialogLaunchAnimator provideDialogLaunchAnimator(Context context,
-            LaunchAnimator launchAnimator, IDreamManager dreamManager) {
-        return new DialogLaunchAnimator(context, launchAnimator, dreamManager);
+    static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager) {
+        return new DialogLaunchAnimator(dreamManager);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
index 64a7305..349b191 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
@@ -2,6 +2,7 @@
 
 import android.util.MathUtils
 import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.LaunchAnimator
 import kotlin.math.min
@@ -55,6 +56,7 @@
         }
 
     fun getProgress(delay: Long, duration: Long): Float {
-        return LaunchAnimator.getProgress(linearProgress, delay, duration)
+        return LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay,
+            duration)
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 8797335..ffe6e4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -683,6 +683,7 @@
         boolean showFooterView = (showDismissView || mController.getVisibleNotificationCount() > 0)
                 && mIsCurrentUserSetup  // see: b/193149550
                 && mStatusBarState != StatusBarState.KEYGUARD
+                && mQsExpansionFraction != 1
                 && !mScreenOffAnimationController.shouldHideNotificationsFooter()
                 && !mIsRemoteInputActive;
         boolean showHistory = Settings.Secure.getIntForUser(mContext.getContentResolver(),
@@ -4769,6 +4770,8 @@
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void setQsExpansionFraction(float qsExpansionFraction) {
+        boolean footerAffected = mQsExpansionFraction != qsExpansionFraction
+                && (mQsExpansionFraction == 1 || qsExpansionFraction == 1);
         mQsExpansionFraction = qsExpansionFraction;
         updateUseRoundedRectClipping();
 
@@ -4777,6 +4780,9 @@
         if (mOwnScrollY > 0) {
             setOwnScrollY((int) MathUtils.lerp(mOwnScrollY, 0, mQsExpansionFraction));
         }
+        if (footerAffected) {
+            updateFooter();
+        }
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 020c423..a64e579 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -109,6 +109,7 @@
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.biometrics.AuthController;
@@ -238,7 +239,8 @@
      */
     private static final int FLING_HIDE = 2;
     private static final long ANIMATION_DELAY_ICON_FADE_IN =
-            LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
+            ActivityLaunchAnimator.TIMINGS.getTotalDuration()
+                    - CollapsedStatusBarFragment.FADE_IN_DURATION
                     - CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
 
     private final DozeParameters mDozeParameters;
@@ -3786,8 +3788,8 @@
     }
 
     public void applyLaunchAnimationProgress(float linearProgress) {
-        boolean hideIcons = LaunchAnimator.getProgress(linearProgress,
-                ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
+        boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS,
+                linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
         if (hideIcons != mHideIconsDuringLaunchAnimation) {
             mHideIconsDuringLaunchAnimation = hideIcons;
             if (!hideIcons) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index 32aae6c..2ba37c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -23,7 +23,8 @@
         delegate.onLaunchAnimationStart(isExpandingFullyAbove)
         statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true)
         if (!isExpandingFullyAbove) {
-            statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt())
+            statusBar.collapsePanelWithDuration(
+                ActivityLaunchAnimator.TIMINGS.totalDuration.toInt())
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index d819fa2..1fe3d44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -46,7 +46,7 @@
 @RunWithLooper
 class ActivityLaunchAnimatorTest : SysuiTestCase() {
     private val launchContainer = LinearLayout(mContext)
-    private val launchAnimator = LaunchAnimator(mContext, isForTesting = true)
+    private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
     @Mock lateinit var callback: ActivityLaunchAnimator.Callback
     @Spy private val controller = TestLaunchAnimatorController(launchContainer)
     @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
index f9ad740..b951345 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -33,7 +33,7 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class DialogLaunchAnimatorTest : SysuiTestCase() {
-    private val launchAnimator = LaunchAnimator(context, isForTesting = true)
+    private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
     private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
     private val attachedViews = mutableSetOf<View>()
 
@@ -42,7 +42,8 @@
 
     @Before
     fun setUp() {
-        dialogLaunchAnimator = DialogLaunchAnimator(context, launchAnimator, dreamManager)
+        dialogLaunchAnimator = DialogLaunchAnimator(
+            dreamManager, launchAnimator, isForTesting = true)
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt
new file mode 100644
index 0000000..dadf94e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt
@@ -0,0 +1,23 @@
+package com.android.systemui.animation
+
+/**
+ * A [LaunchAnimator.Timings] to be used in tests.
+ *
+ * Note that all timings except the total duration are non-zero to avoid divide-by-zero exceptions
+ * when computing the progress of a sub-animation (the contents fade in/out).
+ */
+val TEST_TIMINGS = LaunchAnimator.Timings(
+    totalDuration = 0L,
+    contentBeforeFadeOutDelay = 1L,
+    contentBeforeFadeOutDuration = 1L,
+    contentAfterFadeInDelay = 1L,
+    contentAfterFadeInDuration = 1L
+)
+
+/** A [LaunchAnimator.Interpolators] to be used in tests. */
+val TEST_INTERPOLATORS = LaunchAnimator.Interpolators(
+    positionInterpolator = Interpolators.STANDARD,
+    positionXInterpolator = Interpolators.STANDARD,
+    contentBeforeFadeOutInterpolator = Interpolators.STANDARD,
+    contentAfterFadeInInterpolator = Interpolators.STANDARD
+)
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
index 24e47c5..bd4bfff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
@@ -58,10 +58,9 @@
     }
 
     private QSPanelControllerBase.TileRecord createTileRecord() {
-        QSPanelControllerBase.TileRecord tileRecord = new QSPanelControllerBase.TileRecord();
-        tileRecord.tile = mock(QSTile.class);
-        tileRecord.tileView = spy(new QSTileViewImpl(mContext, new QSIconViewImpl(mContext)));
-        return tileRecord;
+        return new QSPanelControllerBase.TileRecord(
+                mock(QSTile.class),
+                spy(new QSTileViewImpl(mContext, new QSIconViewImpl(mContext))));
     }
 
     @Test
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..61c91f5 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();
     }
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/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/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/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/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index f3fad84..121a0bc 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -42,6 +42,7 @@
 import android.os.Looper;
 import android.os.PersistableBundle;
 import android.os.Process;
+import android.os.storage.StorageManager;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
@@ -872,8 +873,8 @@
      *
      * @throws IOException if the request was unable to be fulfilled.
      */
-    public abstract void freeStorage(String volumeUuid, long bytes, int storageFlags)
-            throws IOException;
+    public abstract void freeStorage(String volumeUuid, long bytes,
+            @StorageManager.AllocateFlags int flags) throws IOException;
 
     /** Returns {@code true} if the specified component is enabled and matches the given flags. */
     public abstract boolean isEnabledAndMatches(@NonNull ParsedMainComponent component,
@@ -1261,5 +1262,6 @@
     /**
      * Reconcile all app data for the given user.
      */
-    public abstract void reconcileAppsData(int userId, int storageFlags, boolean migrateAppsData);
+    public abstract void reconcileAppsData(int userId, @StorageManager.StorageFlags int flags,
+            boolean migrateAppsData);
 }
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/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0f3b082..9c8a663 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1241,6 +1241,10 @@
         initMinStreamVolumeWithoutModifyAudioSettings();
 
         updateVibratorInfos();
+
+        synchronized (mSupportedSystemUsagesLock) {
+            AudioSystem.setSupportedSystemUsages(mSupportedSystemUsages);
+        }
     }
 
     //-----------------------------------------------------------------
@@ -3224,6 +3228,15 @@
         }
     }
 
+    private void enforceCallAudioInterceptionPermission() {
+        if (mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.CALL_AUDIO_INTERCEPTION)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Missing CALL_AUDIO_INTERCEPTION permission");
+        }
+    }
+
+
     /** @see AudioManager#setVolumeIndexForAttributes(attr, int, int) */
     public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags,
             String callingPackage, String attributionTag) {
@@ -4943,6 +4956,26 @@
         mModeDispatchers.unregister(dispatcher);
     }
 
+    /** @see AudioManager#isPstnCallAudioInterceptable() */
+    public boolean isPstnCallAudioInterceptable() {
+        enforceCallAudioInterceptionPermission();
+
+        boolean uplinkDeviceFound = false;
+        boolean downlinkDeviceFound = false;
+        AudioDeviceInfo[] devices = AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_ALL);
+        for (AudioDeviceInfo device : devices) {
+            if (device.getInternalType() == AudioSystem.DEVICE_OUT_TELEPHONY_TX) {
+                uplinkDeviceFound = true;
+            } else if (device.getInternalType() == AudioSystem.DEVICE_IN_TELEPHONY_RX) {
+                downlinkDeviceFound = true;
+            }
+            if (uplinkDeviceFound && downlinkDeviceFound) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /** @see AudioManager#setRttEnabled() */
     @Override
     public void setRttEnabled(boolean rttEnabled) {
@@ -8169,7 +8202,10 @@
     private void validateAudioAttributesUsage(@NonNull AudioAttributes audioAttributes) {
         @AudioAttributes.AttributeUsage int usage = audioAttributes.getSystemUsage();
         if (AudioAttributes.isSystemUsage(usage)) {
-            if (callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)) {
+            if ((usage == AudioAttributes.USAGE_CALL_ASSISTANT
+                    && (audioAttributes.getAllFlags() & AudioAttributes.FLAG_CALL_REDIRECTION) != 0
+                    && callerHasPermission(Manifest.permission.CALL_AUDIO_INTERCEPTION))
+                    || callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)) {
                 if (!isSupportedSystemUsage(usage)) {
                     throw new IllegalArgumentException(
                             "Unsupported usage " + AudioAttributes.usageToString(usage));
@@ -8183,8 +8219,12 @@
     private boolean isValidAudioAttributesUsage(@NonNull AudioAttributes audioAttributes) {
         @AudioAttributes.AttributeUsage int usage = audioAttributes.getSystemUsage();
         if (AudioAttributes.isSystemUsage(usage)) {
-            return callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
-                    && isSupportedSystemUsage(usage);
+            return isSupportedSystemUsage(usage)
+                    && ((usage == AudioAttributes.USAGE_CALL_ASSISTANT
+                        && (audioAttributes.getAllFlags()
+                            & AudioAttributes.FLAG_CALL_REDIRECTION) != 0
+                        && callerHasPermission(Manifest.permission.CALL_AUDIO_INTERCEPTION))
+                        || callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING));
         }
         return true;
     }
@@ -9578,6 +9618,7 @@
         boolean requireValidProjection = false;
         boolean requireCaptureAudioOrMediaOutputPerm = false;
         boolean requireModifyRouting = false;
+        boolean requireCallAudioInterception = false;
         ArrayList<AudioMix> voiceCommunicationCaptureMixes = null;
 
 
@@ -9618,7 +9659,10 @@
             // otherwise MODIFY_AUDIO_ROUTING permission is required
             if (mix.getRouteFlags() == mix.ROUTE_FLAG_LOOP_BACK_RENDER && projection != null) {
                 requireValidProjection |= true;
-            } else {
+            } else if (mix.isForCallRedirection()) {
+                requireCallAudioInterception |= true;
+            } else if (mix.containsMatchAttributeRuleForUsage(
+                            AudioAttributes.USAGE_VOICE_COMMUNICATION)) {
                 requireModifyRouting |= true;
             }
         }
@@ -9655,6 +9699,12 @@
             return false;
         }
 
+        if (requireCallAudioInterception
+                && !callerHasPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION)) {
+            Log.e(TAG, "Can not capture audio without CALL_AUDIO_INTERCEPTION");
+            return false;
+        }
+
         return true;
     }
 
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/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java
index 6ee1981..1e2eb5d 100644
--- a/services/core/java/com/android/server/pm/AppDataHelper.java
+++ b/services/core/java/com/android/server/pm/AppDataHelper.java
@@ -162,7 +162,8 @@
      * </ul>
      */
     private @NonNull CompletableFuture<?> prepareAppData(@NonNull Installer.Batch batch,
-            @Nullable AndroidPackage pkg, int previousAppId, int userId, int flags) {
+            @Nullable AndroidPackage pkg, int previousAppId, int userId,
+            @StorageManager.StorageFlags int flags) {
         if (pkg == null) {
             Slog.wtf(TAG, "Package was null!", new Throwable());
             return CompletableFuture.completedFuture(null);
@@ -171,7 +172,8 @@
     }
 
     private void prepareAppDataAndMigrate(@NonNull Installer.Batch batch,
-            @NonNull AndroidPackage pkg, int userId, int flags, boolean maybeMigrateAppData) {
+            @NonNull AndroidPackage pkg, int userId, @StorageManager.StorageFlags int flags,
+            boolean maybeMigrateAppData) {
         prepareAppData(batch, pkg, Process.INVALID_UID, userId, flags).thenRun(() -> {
             // Note: this code block is executed with the Installer lock
             // already held, since it's invoked as a side-effect of
@@ -331,7 +333,8 @@
      * correct for all installed apps on all mounted volumes.
      */
     @NonNull
-    public void reconcileAppsData(int userId, int flags, boolean migrateAppsData) {
+    public void reconcileAppsData(int userId, @StorageManager.StorageFlags int flags,
+            boolean migrateAppsData) {
         final StorageManager storage = mInjector.getSystemService(StorageManager.class);
         for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
             final String volumeUuid = vol.getFsUuid();
@@ -342,7 +345,7 @@
     }
 
     @GuardedBy("mPm.mInstallLock")
-    void reconcileAppsDataLI(String volumeUuid, int userId, int flags,
+    void reconcileAppsDataLI(String volumeUuid, int userId, @StorageManager.StorageFlags int flags,
             boolean migrateAppData) {
         reconcileAppsDataLI(volumeUuid, userId, flags, migrateAppData, false /* onlyCoreApps */);
     }
@@ -359,8 +362,8 @@
      * @return list of skipped non-core packages (if {@code onlyCoreApps} is true)
      */
     @GuardedBy("mPm.mInstallLock")
-    private List<String> reconcileAppsDataLI(String volumeUuid, int userId, int flags,
-            boolean migrateAppData, boolean onlyCoreApps) {
+    private List<String> reconcileAppsDataLI(String volumeUuid, int userId,
+            @StorageManager.StorageFlags int flags, boolean migrateAppData, boolean onlyCoreApps) {
         Slog.v(TAG, "reconcileAppsData for " + volumeUuid + " u" + userId + " 0x"
                 + Integer.toHexString(flags) + " migrateAppData=" + migrateAppData);
         List<String> result = onlyCoreApps ? new ArrayList<>() : null;
@@ -479,7 +482,7 @@
      * can't wait for user to start
      */
     public Future<?> fixAppsDataOnBoot() {
-        final int storageFlags;
+        final @StorageManager.StorageFlags int storageFlags;
         if (StorageManager.isFileEncryptedNativeOrEmulated()) {
             storageFlags = StorageManager.FLAG_STORAGE_DE;
         } else {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 80c2a7e..fce37a9 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2322,12 +2322,19 @@
                     }
                 }
 
-                // It's implied that when a user requests installation, they want the app to be
-                // installed and enabled. (This does not apply to USER_ALL, which here means only
-                // install on users for which the app is already installed).
                 if (userId != UserHandle.USER_ALL) {
+                    // It's implied that when a user requests installation, they want the app to
+                    // be installed and enabled.
                     ps.setInstalled(true, userId);
                     ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT, userId, installerPackageName);
+                } else if (allUsers != null) {
+                    // The caller explicitly specified INSTALL_ALL_USERS flag.
+                    // Thus, updating the settings to install the app for all users.
+                    for (int currentUserId : allUsers) {
+                        ps.setInstalled(true, currentUserId);
+                        ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT, userId,
+                                installerPackageName);
+                    }
                 }
 
                 mPm.mSettings.addInstallerPackageNames(ps.getInstallSource());
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 333d3a4..fa23000 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -2741,13 +2741,13 @@
 
     @Override
     public void freeStorageAndNotify(final String volumeUuid, final long freeStorageSize,
-            final int storageFlags, final IPackageDataObserver observer) {
+            final @StorageManager.AllocateFlags int flags, final IPackageDataObserver observer) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.CLEAR_APP_CACHE, null);
         mHandler.post(() -> {
             boolean success = false;
             try {
-                freeStorage(volumeUuid, freeStorageSize, storageFlags);
+                freeStorage(volumeUuid, freeStorageSize, flags);
                 success = true;
             } catch (IOException e) {
                 Slog.w(TAG, e);
@@ -2764,13 +2764,13 @@
 
     @Override
     public void freeStorage(final String volumeUuid, final long freeStorageSize,
-            final int storageFlags, final IntentSender pi) {
+            final @StorageManager.AllocateFlags int flags, final IntentSender pi) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.CLEAR_APP_CACHE, TAG);
         mHandler.post(() -> {
             boolean success = false;
             try {
-                freeStorage(volumeUuid, freeStorageSize, storageFlags);
+                freeStorage(volumeUuid, freeStorageSize, flags);
                 success = true;
             } catch (IOException e) {
                 Slog.w(TAG, e);
@@ -2789,7 +2789,8 @@
      * Blocking call to clear various types of cached data across the system
      * until the requested bytes are available.
      */
-    public void freeStorage(String volumeUuid, long bytes, int storageFlags) throws IOException {
+    public void freeStorage(String volumeUuid, long bytes,
+            @StorageManager.AllocateFlags int flags) throws IOException {
         final StorageManager storage = mInjector.getSystemService(StorageManager.class);
         final File file = storage.findPathForUuid(volumeUuid);
         if (file.getUsableSpace() >= bytes) return;
@@ -2797,8 +2798,7 @@
         if (mEnableFreeCacheV2) {
             final boolean internalVolume = Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL,
                     volumeUuid);
-            final boolean aggressive = (storageFlags
-                    & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0;
+            final boolean aggressive = (flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0;
 
             // 1. Pre-flight to determine if we have any chance to succeed
             // 2. Consider preloaded data (after 1w honeymoon, unless aggressive)
@@ -8311,9 +8311,9 @@
         }
 
         @Override
-        public void freeStorage(String volumeUuid, long bytes, int storageFlags)
-                throws IOException {
-            PackageManagerService.this.freeStorage(volumeUuid, bytes, storageFlags);
+        public void freeStorage(String volumeUuid, long bytes,
+                @StorageManager.AllocateFlags int flags) throws IOException {
+            PackageManagerService.this.freeStorage(volumeUuid, bytes, flags);
         }
 
         @Override
@@ -8695,7 +8695,8 @@
         }
 
         @Override
-        public void reconcileAppsData(int userId, int flags, boolean migrateAppsData) {
+        public void reconcileAppsData(int userId, @StorageManager.StorageFlags int flags,
+                boolean migrateAppsData) {
             PackageManagerService.this.mAppDataHelper.reconcileAppsData(userId, flags,
                     migrateAppsData);
         }
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/Task.java b/services/core/java/com/android/server/wm/Task.java
index f0b55cb..ea99781 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1440,11 +1440,11 @@
     }
 
     /** Called when an {@link ActivityRecord} is added as a descendant */
-    void onDescendantActivityAdded(boolean hadChild, int activityType, ActivityRecord r) {
+    void onDescendantActivityAdded(boolean hadActivity, int activityType, ActivityRecord r) {
         warnForNonLeafTask("onDescendantActivityAdded");
 
         // Only set this based on the first activity
-        if (!hadChild) {
+        if (!hadActivity) {
             if (r.getActivityType() == ACTIVITY_TYPE_UNDEFINED) {
                 // Normally non-standard activity type for the activity record will be set when the
                 // object is created, however we delay setting the standard application type until
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 59a5cdf..fdaa2fc 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -101,6 +101,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -1666,8 +1667,8 @@
         boolean isAddingActivity = child.asActivityRecord() != null;
         final Task task = isAddingActivity ? getTask() : null;
 
-        // If this task had any child before we added this one.
-        boolean taskHadChild = task != null && task.hasChild();
+        // If this task had any activity before we added this one.
+        boolean taskHadActivity = task != null && task.getActivity(Objects::nonNull) != null;
         // getActivityType() looks at the top child, so we need to read the type before adding
         // a new child in case the new child is on top and UNDEFINED.
         final int activityType = task != null ? task.getActivityType() : ACTIVITY_TYPE_UNDEFINED;
@@ -1676,7 +1677,7 @@
 
         if (isAddingActivity && task != null) {
             child.asActivityRecord().inHistory = true;
-            task.onDescendantActivityAdded(taskHadChild, activityType, child.asActivityRecord());
+            task.onDescendantActivityAdded(taskHadActivity, activityType, child.asActivityRecord());
         }
     }
 
@@ -2186,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/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/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/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/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());