Merge "Fix work profile applications do not receive event transaction data"
diff --git a/boot/boot-image-profile.txt b/boot/boot-image-profile.txt
index 5f27cc7..82269d4 100644
--- a/boot/boot-image-profile.txt
+++ b/boot/boot-image-profile.txt
@@ -2899,10 +2899,8 @@
 HSPLandroid/app/assist/AssistStructure$ViewNode;->getChildCount()I
 HSPLandroid/app/assist/AssistStructure$ViewNode;->writeSelfToParcel(Landroid/os/Parcel;Landroid/os/PooledStringWriter;Z[FZ)I+]Landroid/view/autofill/AutofillId;Landroid/view/autofill/AutofillId;]Landroid/graphics/Matrix;Landroid/graphics/Matrix;]Landroid/app/assist/AssistStructure$ViewNodeText;Landroid/app/assist/AssistStructure$ViewNodeText;]Landroid/os/Parcel;Landroid/os/Parcel;
 HSPLandroid/app/assist/AssistStructure$ViewNode;->writeString(Landroid/os/Parcel;Landroid/os/PooledStringWriter;Ljava/lang/String;)V+]Landroid/os/PooledStringWriter;Landroid/os/PooledStringWriter;]Landroid/os/Parcel;Landroid/os/Parcel;
-HSPLandroid/app/assist/AssistStructure$ViewNodeBuilder;-><init>()V
 HSPLandroid/app/assist/AssistStructure$ViewNodeBuilder;->getChildCount()I
 HSPLandroid/app/assist/AssistStructure$ViewNodeBuilder;->getNodeText()Landroid/app/assist/AssistStructure$ViewNodeText;
-HSPLandroid/app/assist/AssistStructure$ViewNodeBuilder;->getViewNode()Landroid/app/assist/AssistStructure$ViewNode;
 HSPLandroid/app/assist/AssistStructure$ViewNodeBuilder;->newChild(I)Landroid/view/ViewStructure;
 HSPLandroid/app/assist/AssistStructure$ViewNodeBuilder;->setAutofillHints([Ljava/lang/String;)V
 HSPLandroid/app/assist/AssistStructure$ViewNodeBuilder;->setAutofillId(Landroid/view/autofill/AutofillId;)V
@@ -3883,8 +3881,6 @@
 HSPLandroid/content/ContentResolver$ResultListener;-><init>(Landroid/content/ContentResolver$1;)V
 HSPLandroid/content/ContentResolver$ResultListener;->onResult(Landroid/os/Bundle;)V+]Landroid/os/ParcelableException;Landroid/os/ParcelableException;]Ljava/lang/Object;Landroid/content/ContentResolver$StringResultListener;,Landroid/content/ContentResolver$UriResultListener;]Landroid/os/Bundle;Landroid/os/Bundle;]Landroid/content/ContentResolver$ResultListener;Landroid/content/ContentResolver$UriResultListener;,Landroid/content/ContentResolver$StringResultListener;
 HSPLandroid/content/ContentResolver$ResultListener;->waitForResult(J)V+]Ljava/lang/Object;Landroid/content/ContentResolver$StringResultListener;
-HSPLandroid/content/ContentResolver$StringResultListener;-><init>()V
-HSPLandroid/content/ContentResolver$StringResultListener;-><init>(Landroid/content/ContentResolver$1;)V
 HSPLandroid/content/ContentResolver$StringResultListener;->getResultFromBundle(Landroid/os/Bundle;)Ljava/lang/Object;+]Landroid/content/ContentResolver$StringResultListener;Landroid/content/ContentResolver$StringResultListener;
 HSPLandroid/content/ContentResolver$StringResultListener;->getResultFromBundle(Landroid/os/Bundle;)Ljava/lang/String;+]Landroid/os/Bundle;Landroid/os/Bundle;
 HSPLandroid/content/ContentResolver;-><init>(Landroid/content/Context;)V
@@ -7029,7 +7025,6 @@
 HSPLandroid/graphics/Region$1;->createFromParcel(Landroid/os/Parcel;)Landroid/graphics/Region;
 HSPLandroid/graphics/Region$1;->createFromParcel(Landroid/os/Parcel;)Ljava/lang/Object;+]Landroid/graphics/Region$1;Landroid/graphics/Region$1;
 HSPLandroid/graphics/Region;-><init>()V
-HSPLandroid/graphics/Region;-><init>(IIII)V
 HSPLandroid/graphics/Region;-><init>(J)V
 HSPLandroid/graphics/Region;->access$000(Landroid/os/Parcel;)J
 HSPLandroid/graphics/Region;->equals(Ljava/lang/Object;)Z
@@ -7075,7 +7070,6 @@
 HSPLandroid/graphics/RenderNode;->hasDisplayList()Z
 HSPLandroid/graphics/RenderNode;->hasIdentityMatrix()Z
 HSPLandroid/graphics/RenderNode;->isAttached()Z+]Landroid/graphics/RenderNode$AnimationHost;Landroid/view/ViewAnimationHostBridge;
-HSPLandroid/graphics/RenderNode;->isPivotExplicitlySet()Z
 HSPLandroid/graphics/RenderNode;->offsetTopAndBottom(I)Z
 HSPLandroid/graphics/RenderNode;->setAlpha(F)Z
 HSPLandroid/graphics/RenderNode;->setAnimationMatrix(Landroid/graphics/Matrix;)Z+]Landroid/graphics/Matrix;Landroid/graphics/Matrix;
@@ -8283,7 +8277,6 @@
 HSPLandroid/hardware/GeomagneticField$LegendreTable;-><init>(IF)V
 HSPLandroid/hardware/GeomagneticField;-><init>(FFFJ)V
 HSPLandroid/hardware/GeomagneticField;->computeGeocentricCoordinates(FFF)V
-HSPLandroid/hardware/GeomagneticField;->getDeclination()F
 HSPLandroid/hardware/HardwareBuffer$1;->createFromParcel(Landroid/os/Parcel;)Landroid/hardware/HardwareBuffer;
 HSPLandroid/hardware/HardwareBuffer$1;->createFromParcel(Landroid/os/Parcel;)Ljava/lang/Object;
 HSPLandroid/hardware/HardwareBuffer;-><init>(J)V+]Llibcore/util/NativeAllocationRegistry;Llibcore/util/NativeAllocationRegistry;]Ljava/lang/Class;Ljava/lang/Class;]Ldalvik/system/CloseGuard;Ldalvik/system/CloseGuard;
@@ -11353,7 +11346,6 @@
 HSPLandroid/media/SoundPool$EventHandler;->handleMessage(Landroid/os/Message;)V
 HSPLandroid/media/SoundPool;-><init>(ILandroid/media/AudioAttributes;)V
 HSPLandroid/media/SoundPool;-><init>(ILandroid/media/AudioAttributes;Landroid/media/SoundPool$1;)V
-HSPLandroid/media/SoundPool;->load(Ljava/lang/String;I)I
 HSPLandroid/media/SoundPool;->postEventFromNative(Ljava/lang/Object;IIILjava/lang/Object;)V
 HSPLandroid/media/SoundPool;->setOnLoadCompleteListener(Landroid/media/SoundPool$OnLoadCompleteListener;)V
 HSPLandroid/media/SubtitleController$1;->handleMessage(Landroid/os/Message;)Z
@@ -11389,10 +11381,8 @@
 HSPLandroid/media/Utils;->sortDistinctRanges([Landroid/util/Range;)V
 HSPLandroid/media/audiofx/AudioEffect$Descriptor;-><init>(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
 HSPLandroid/media/audiopolicy/AudioProductStrategy$AudioAttributesGroup;-><init>(II[Landroid/media/AudioAttributes;)V
-HSPLandroid/media/audiopolicy/AudioProductStrategy$AudioAttributesGroup;->supportsStreamType(I)Z
 HSPLandroid/media/audiopolicy/AudioProductStrategy;-><init>(Ljava/lang/String;I[Landroid/media/audiopolicy/AudioProductStrategy$AudioAttributesGroup;)V
 HSPLandroid/media/audiopolicy/AudioProductStrategy;->attributesMatches(Landroid/media/AudioAttributes;Landroid/media/AudioAttributes;)Z+]Landroid/media/AudioAttributes;Landroid/media/AudioAttributes;
-HSPLandroid/media/audiopolicy/AudioProductStrategy;->getAudioAttributesForLegacyStreamType(I)Landroid/media/AudioAttributes;+]Landroid/media/audiopolicy/AudioProductStrategy$AudioAttributesGroup;Landroid/media/audiopolicy/AudioProductStrategy$AudioAttributesGroup;
 HSPLandroid/media/audiopolicy/AudioProductStrategy;->getAudioAttributesForStrategyWithLegacyStreamType(I)Landroid/media/AudioAttributes;
 HSPLandroid/media/audiopolicy/AudioProductStrategy;->getAudioProductStrategies()Ljava/util/List;
 HSPLandroid/media/audiopolicy/AudioProductStrategy;->getLegacyStreamTypeForStrategyWithAudioAttributes(Landroid/media/AudioAttributes;)I+]Landroid/media/audiopolicy/AudioProductStrategy;Landroid/media/audiopolicy/AudioProductStrategy;]Ljava/util/List;Ljava/util/ArrayList;]Ljava/util/Iterator;Ljava/util/ArrayList$Itr;
@@ -13125,7 +13115,6 @@
 HSPLandroid/os/ParcelableParcel$1;->createFromParcel(Landroid/os/Parcel;)Landroid/os/ParcelableParcel;
 HSPLandroid/os/ParcelableParcel$1;->createFromParcel(Landroid/os/Parcel;)Ljava/lang/Object;
 HSPLandroid/os/ParcelableParcel;-><init>(Landroid/os/Parcel;Ljava/lang/ClassLoader;)V
-HSPLandroid/os/ParcelableParcel;-><init>(Ljava/lang/ClassLoader;)V
 HSPLandroid/os/ParcelableParcel;->getClassLoader()Ljava/lang/ClassLoader;
 HSPLandroid/os/ParcelableParcel;->getParcel()Landroid/os/Parcel;
 HSPLandroid/os/ParcelableParcel;->writeToParcel(Landroid/os/Parcel;I)V
@@ -13481,7 +13470,6 @@
 HSPLandroid/os/Temperature;-><init>(FILjava/lang/String;I)V
 HSPLandroid/os/Temperature;->getStatus()I
 HSPLandroid/os/Temperature;->isValidStatus(I)Z
-HSPLandroid/os/Temperature;->isValidType(I)Z
 HSPLandroid/os/ThreadLocalWorkSource$$ExternalSyntheticLambda0;->get()Ljava/lang/Object;
 HSPLandroid/os/ThreadLocalWorkSource;->getToken()J+]Ljava/lang/Integer;Ljava/lang/Integer;]Ljava/lang/ThreadLocal;Ljava/lang/ThreadLocal$SuppliedThreadLocal;
 HSPLandroid/os/ThreadLocalWorkSource;->getUid()I+]Ljava/lang/Integer;Ljava/lang/Integer;]Ljava/lang/ThreadLocal;Ljava/lang/ThreadLocal$SuppliedThreadLocal;
@@ -18488,7 +18476,6 @@
 HSPLandroid/view/ViewRootImpl$ImeInputStage;-><init>(Landroid/view/ViewRootImpl;Landroid/view/ViewRootImpl$InputStage;Ljava/lang/String;)V
 HSPLandroid/view/ViewRootImpl$ImeInputStage;->onFinishedInputEvent(Ljava/lang/Object;Z)V
 HSPLandroid/view/ViewRootImpl$ImeInputStage;->onProcess(Landroid/view/ViewRootImpl$QueuedInputEvent;)I
-HSPLandroid/view/ViewRootImpl$InputMetricsListener;-><init>(Landroid/view/ViewRootImpl;)V
 HSPLandroid/view/ViewRootImpl$InputMetricsListener;->onFrameMetricsAvailable(I)V+]Landroid/view/ViewRootImpl$WindowInputEventReceiver;Landroid/view/ViewRootImpl$WindowInputEventReceiver;]Ljava/lang/StringBuilder;Ljava/lang/StringBuilder;]Landroid/view/InputEventReceiver;Landroid/view/ViewRootImpl$WindowInputEventReceiver;
 HSPLandroid/view/ViewRootImpl$InputStage;-><init>(Landroid/view/ViewRootImpl;Landroid/view/ViewRootImpl$InputStage;)V
 HSPLandroid/view/ViewRootImpl$InputStage;->apply(Landroid/view/ViewRootImpl$QueuedInputEvent;I)V+]Landroid/view/ViewRootImpl$InputStage;megamorphic_types
@@ -20395,7 +20382,6 @@
 HSPLandroid/widget/OverScroller$SplineOverScroller;->fling(IIIII)V
 HSPLandroid/widget/OverScroller$SplineOverScroller;->getSplineDeceleration(I)D
 HSPLandroid/widget/OverScroller$SplineOverScroller;->getSplineFlingDistance(I)D
-HSPLandroid/widget/OverScroller$SplineOverScroller;->getSplineFlingDuration(I)I
 HSPLandroid/widget/OverScroller$SplineOverScroller;->onEdgeReached()V
 HSPLandroid/widget/OverScroller$SplineOverScroller;->springback(III)Z
 HSPLandroid/widget/OverScroller$SplineOverScroller;->startScroll(III)V
@@ -22568,10 +22554,7 @@
 HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker$1;->onIsNonStrongBiometricAllowedChanged(ZI)V
 HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker$1;->onStrongAuthRequiredChanged(II)V
 HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker$H;->handleMessage(Landroid/os/Message;)V
-HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker;-><init>(Landroid/content/Context;)V
-HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker;-><init>(Landroid/content/Context;Landroid/os/Looper;)V
 HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker;->getStrongAuthForUser(I)I+]Landroid/util/SparseIntArray;Landroid/util/SparseIntArray;
-HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker;->getStub()Landroid/app/trust/IStrongAuthTracker$Stub;
 HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker;->handleIsNonStrongBiometricAllowedChanged(ZI)V
 HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker;->handleStrongAuthRequiredChanged(II)V
 HSPLcom/android/internal/widget/LockPatternUtils$StrongAuthTracker;->isNonStrongBiometricAllowedAfterIdleTimeout(I)Z
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index cf00cbd..2c875fe 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -51,7 +51,6 @@
 import android.content.Attributable;
 import android.content.AttributionSource;
 import android.content.Context;
-import android.os.BatteryStats;
 import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
@@ -59,7 +58,6 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
-import android.os.SynchronousResultReceiver;
 import android.os.SystemProperties;
 import android.util.Log;
 import android.util.Pair;
@@ -82,7 +80,6 @@
 import java.util.UUID;
 import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
-import java.util.concurrent.TimeoutException;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 /**
@@ -2424,38 +2421,6 @@
     }
 
     /**
-     * Return the record of {@link BluetoothActivityEnergyInfo} object that
-     * has the activity and energy info. This can be used to ascertain what
-     * the controller has been up to, since the last sample.
-     *
-     * @param updateType Type of info, cached vs refreshed.
-     * @return a record with {@link BluetoothActivityEnergyInfo} or null if report is unavailable or
-     * unsupported
-     * @hide
-     * @deprecated use the asynchronous {@link #requestControllerActivityEnergyInfo(ResultReceiver)}
-     * instead.
-     */
-    @Deprecated
-    @RequiresBluetoothConnectPermission
-    @RequiresPermission(allOf = {
-            android.Manifest.permission.BLUETOOTH_CONNECT,
-            android.Manifest.permission.BLUETOOTH_PRIVILEGED,
-    })
-    public BluetoothActivityEnergyInfo getControllerActivityEnergyInfo(int updateType) {
-        SynchronousResultReceiver receiver = new SynchronousResultReceiver();
-        requestControllerActivityEnergyInfo(receiver);
-        try {
-            SynchronousResultReceiver.Result result = receiver.awaitResult(1000);
-            if (result.bundle != null) {
-                return result.bundle.getParcelable(BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY);
-            }
-        } catch (TimeoutException e) {
-            Log.e(TAG, "getControllerActivityEnergyInfo timed out");
-        }
-        return null;
-    }
-
-    /**
      * Request the record of {@link BluetoothActivityEnergyInfo} object that
      * has the activity and energy info. This can be used to ascertain what
      * the controller has been up to, since the last sample.
diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java
index ee24084..c906a13 100644
--- a/core/java/android/net/NetworkTemplate.java
+++ b/core/java/android/net/NetworkTemplate.java
@@ -70,9 +70,17 @@
     private static final String TAG = "NetworkTemplate";
 
     /**
+     * Initial Version of the backup serializer.
+     */
+    public static final int BACKUP_VERSION_1_INIT = 1;
+    /**
+     * Version of the backup serializer that added carrier template support.
+     */
+    public static final int BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE = 2;
+    /**
      * Current Version of the Backup Serializer.
      */
-    private static final int BACKUP_VERSION = 1;
+    private static final int BACKUP_VERSION = BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE;
 
     public static final int MATCH_MOBILE = 1;
     public static final int MATCH_WIFI = 4;
@@ -285,6 +293,10 @@
     private final int mRoaming;
     private final int mDefaultNetwork;
     private final int mSubType;
+    /**
+     * The subscriber Id match rule defines how the template should match networks with
+     * specific subscriberId(s). See NetworkTemplate#SUBSCRIBER_ID_MATCH_RULE_* for more detail.
+     */
     private final int mSubscriberIdMatchRule;
 
     // Bitfield containing OEM network properties{@code NetworkIdentity#OEM_*}.
@@ -348,7 +360,7 @@
         mSubscriberIdMatchRule = subscriberIdMatchRule;
         checkValidSubscriberIdMatchRule();
         if (!isKnownMatchRule(matchRule)) {
-            Log.e(TAG, "Unknown network template rule " + matchRule
+            throw new IllegalArgumentException("Unknown network template rule " + matchRule
                     + " will not match any identity.");
         }
     }
@@ -842,11 +854,17 @@
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         DataOutputStream out = new DataOutputStream(baos);
 
+        if (!isPersistable()) {
+            Log.wtf(TAG, "Trying to backup non-persistable template: " + this);
+        }
+
         out.writeInt(BACKUP_VERSION);
 
         out.writeInt(mMatchRule);
         BackupUtils.writeString(out, mSubscriberId);
         BackupUtils.writeString(out, mNetworkId);
+        out.writeInt(mMetered);
+        out.writeInt(mSubscriberIdMatchRule);
 
         return baos.toByteArray();
     }
@@ -854,7 +872,7 @@
     public static NetworkTemplate getNetworkTemplateFromBackup(DataInputStream in)
             throws IOException, BackupUtils.BadVersionException {
         int version = in.readInt();
-        if (version < 1 || version > BACKUP_VERSION) {
+        if (version < BACKUP_VERSION_1_INIT || version > BACKUP_VERSION) {
             throw new BackupUtils.BadVersionException("Unknown Backup Serialization Version");
         }
 
@@ -862,11 +880,27 @@
         String subscriberId = BackupUtils.readString(in);
         String networkId = BackupUtils.readString(in);
 
-        if (!isKnownMatchRule(matchRule)) {
-            throw new BackupUtils.BadVersionException(
-                    "Restored network template contains unknown match rule " + matchRule);
+        final int metered;
+        final int subscriberIdMatchRule;
+        if (version >= BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE) {
+            metered = in.readInt();
+            subscriberIdMatchRule = in.readInt();
+        } else {
+            // For backward compatibility, fill the missing filters from match rules.
+            metered = (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD
+                    || matchRule == MATCH_CARRIER) ? METERED_YES : METERED_ALL;
+            subscriberIdMatchRule = SUBSCRIBER_ID_MATCH_RULE_EXACT;
         }
 
-        return new NetworkTemplate(matchRule, subscriberId, networkId);
+        try {
+            return new NetworkTemplate(matchRule,
+                    subscriberId, new String[] { subscriberId },
+                    networkId, metered, NetworkStats.ROAMING_ALL,
+                    NetworkStats.DEFAULT_NETWORK_ALL, NetworkTemplate.NETWORK_TYPE_ALL,
+                    NetworkTemplate.OEM_MANAGED_ALL, subscriberIdMatchRule);
+        } catch (IllegalArgumentException e) {
+            throw new BackupUtils.BadVersionException(
+                    "Restored network template contains unknown match rule " + matchRule, e);
+        }
     }
 }
diff --git a/core/java/android/os/storage/VolumeInfo.java b/core/java/android/os/storage/VolumeInfo.java
index f57d157..39a2e13 100644
--- a/core/java/android/os/storage/VolumeInfo.java
+++ b/core/java/android/os/storage/VolumeInfo.java
@@ -56,11 +56,12 @@
  * <ul>
  * <li>{@link #MOUNT_FLAG_PRIMARY} means the volume provides primary external
  * storage, historically found at {@code /sdcard}.
- * <li>{@link #MOUNT_FLAG_VISIBLE} means the volume is visible to third-party
- * apps for direct filesystem access. The system should send out relevant
- * storage broadcasts and index any media on visible volumes. Visible volumes
- * are considered a more stable part of the device, which is why we take the
- * time to index them. In particular, transient volumes like USB OTG devices
+ * <li>{@link #MOUNT_FLAG_VISIBLE_FOR_READ} and
+ * {@link #MOUNT_FLAG_VISIBLE_FOR_WRITE} mean the volume is visible to
+ * third-party apps for direct filesystem access. The system should send out
+ * relevant storage broadcasts and index any media on visible volumes. Visible
+ * volumes are considered a more stable part of the device, which is why we take
+ * the time to index them. In particular, transient volumes like USB OTG devices
  * <em>should not</em> be marked as visible; their contents should be surfaced
  * to apps through the Storage Access Framework.
  * </ul>
@@ -100,7 +101,8 @@
     public static final int STATE_BAD_REMOVAL = IVold.VOLUME_STATE_BAD_REMOVAL;
 
     public static final int MOUNT_FLAG_PRIMARY = IVold.MOUNT_FLAG_PRIMARY;
-    public static final int MOUNT_FLAG_VISIBLE = IVold.MOUNT_FLAG_VISIBLE;
+    public static final int MOUNT_FLAG_VISIBLE_FOR_READ = IVold.MOUNT_FLAG_VISIBLE_FOR_READ;
+    public static final int MOUNT_FLAG_VISIBLE_FOR_WRITE = IVold.MOUNT_FLAG_VISIBLE_FOR_WRITE;
 
     private static SparseArray<String> sStateToEnvironment = new SparseArray<>();
     private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>();
@@ -312,17 +314,31 @@
         return isPrimary() && (getType() == TYPE_PUBLIC);
     }
 
-    @UnsupportedAppUsage
-    public boolean isVisible() {
-        return (mountFlags & MOUNT_FLAG_VISIBLE) != 0;
+    private boolean isVisibleForRead() {
+        return (mountFlags & MOUNT_FLAG_VISIBLE_FOR_READ) != 0;
     }
 
-    public boolean isVisibleForUser(int userId) {
-        if ((type == TYPE_PUBLIC || type == TYPE_STUB || type == TYPE_EMULATED)
-                && mountUserId == userId) {
-            return isVisible();
+    private boolean isVisibleForWrite() {
+        return (mountFlags & MOUNT_FLAG_VISIBLE_FOR_WRITE) != 0;
+    }
+
+    @UnsupportedAppUsage
+    public boolean isVisible() {
+        return isVisibleForRead() || isVisibleForWrite();
+    }
+
+    private boolean isVolumeSupportedForUser(int userId) {
+        if (mountUserId != userId) {
+            return false;
         }
-        return false;
+        return type == TYPE_PUBLIC || type == TYPE_STUB || type == TYPE_EMULATED;
+    }
+
+    /**
+     * Returns {@code true} if this volume is visible for {@code userId}, {@code false} otherwise.
+     */
+    public boolean isVisibleForUser(int userId) {
+        return isVolumeSupportedForUser(userId) && isVisible();
     }
 
     /**
@@ -335,12 +351,12 @@
     }
 
     public boolean isVisibleForRead(int userId) {
-        return isVisibleForUser(userId);
+        return isVolumeSupportedForUser(userId) && isVisibleForRead();
     }
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public boolean isVisibleForWrite(int userId) {
-        return isVisibleForUser(userId);
+        return isVolumeSupportedForUser(userId) && isVisibleForWrite();
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/util/BackupUtils.java b/core/java/android/util/BackupUtils.java
index 474ceda..4fcb13c 100644
--- a/core/java/android/util/BackupUtils.java
+++ b/core/java/android/util/BackupUtils.java
@@ -37,6 +37,10 @@
         public BadVersionException(String message) {
             super(message);
         }
+
+        public BadVersionException(String message, Throwable throwable) {
+            super(message, throwable);
+        }
     }
 
     public static String readString(DataInputStream in) throws IOException {
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index 78787e5..65d073f 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -69,6 +69,9 @@
 # Although marked "view" this is mostly graphics stuff
 per-file android_view_* = file:/graphics/java/android/graphics/OWNERS
 
+# Verity
+per-file com_android_internal_security_Verity* = ebiggers@google.com, victorhsieh@google.com
+
 # VINTF
 per-file android_os_VintfObject* = file:platform/system/libvintf:/OWNERS
 per-file android_os_VintfRuntimeInfo* = file:platform/system/libvintf:/OWNERS
diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp
index 4c2b114..5e0d9b3 100644
--- a/core/jni/android_os_Debug.cpp
+++ b/core/jni/android_os_Debug.cpp
@@ -34,6 +34,7 @@
 #include <vector>
 
 #include <android-base/logging.h>
+#include <android-base/properties.h>
 #include <bionic/malloc.h>
 #include <debuggerd/client.h>
 #include <log/log.h>
@@ -859,7 +860,22 @@
     return poolsSizeKb;
 }
 
+static bool halSupportsGpuPrivateMemory() {
+    int productApiLevel =
+            android::base::GetIntProperty("ro.product.first_api_level",
+                                          android::base::GetIntProperty("ro.build.version.sdk",
+                                                                         __ANDROID_API_FUTURE__));
+    int boardApiLevel =
+            android::base::GetIntProperty("ro.board.api_level",
+                                          android::base::GetIntProperty("ro.board.first_api_level",
+                                                                         __ANDROID_API_FUTURE__));
+
+    return std::min(productApiLevel, boardApiLevel) >= __ANDROID_API_S__;
+}
+
 static jlong android_os_Debug_getGpuPrivateMemoryKb(JNIEnv* env, jobject clazz) {
+    static bool gpuPrivateMemorySupported = halSupportsGpuPrivateMemory();
+
     struct memtrack_proc* p = memtrack_proc_new();
     if (p == nullptr) {
         LOG(ERROR) << "getGpuPrivateMemoryKb: Failed to create memtrack_proc";
@@ -876,6 +892,12 @@
     ssize_t gpuPrivateMem = memtrack_proc_gl_pss(p);
 
     memtrack_proc_destroy(p);
+
+    // Old HAL implementations may return 0 for GPU private memory if not supported
+    if (gpuPrivateMem == 0 && !gpuPrivateMemorySupported) {
+        return -1;
+    }
+
     return gpuPrivateMem / 1024;
 }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
index e6d088e..2adf8ce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
@@ -272,8 +272,10 @@
         final String innerPrefix = prefix + "  ";
         final String childPrefix = innerPrefix + "  ";
         pw.println(prefix + this);
-        pw.println(innerPrefix + "Root taskId=" + getRootTaskId()
-                + " winMode=" + mRootTaskInfo.getWindowingMode());
+        if (mRootTaskInfo != null) {
+            pw.println(innerPrefix + "Root taskId=" + mRootTaskInfo.taskId
+                    + " winMode=" + mRootTaskInfo.getWindowingMode());
+        }
         if (mTaskInfo1 != null) {
             pw.println(innerPrefix + "1 taskId=" + mTaskInfo1.taskId
                     + " winMode=" + mTaskInfo1.getWindowingMode());
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 801b490..a3eb0ecc 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -251,7 +251,7 @@
             if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
                 root.flags |= Root.FLAG_HAS_SETTINGS;
             }
-            if (volume.isVisibleForRead(userId)) {
+            if (volume.isVisibleForUser(userId)) {
                 root.visiblePath = volume.getPathForUser(userId);
             } else {
                 root.visiblePath = null;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index 1df1bce..389892e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -307,7 +307,8 @@
             CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
             if (cachedDevice == null) {
                 cachedDevice = mDeviceManager.addDevice(device);
-                Log.d(TAG, "DeviceFoundHandler created new CachedBluetoothDevice");
+                Log.d(TAG, "DeviceFoundHandler created new CachedBluetoothDevice "
+                        + cachedDevice.getDevice().getAnonymizedAddress());
             } else if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
                     && !cachedDevice.getDevice().isConnected()) {
                 // Dispatch device add callback to show bonded but
diff --git a/services/core/java/com/android/server/AppFuseMountException.java b/services/core/java/com/android/server/AppFuseMountException.java
new file mode 100644
index 0000000..9a9379e
--- /dev/null
+++ b/services/core/java/com/android/server/AppFuseMountException.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+import android.os.Parcel;
+
+/**
+ * An exception that indicates there was an error with a
+ * app fuse mount operation.
+ */
+public class AppFuseMountException extends Exception {
+    public AppFuseMountException(String detailMessage) {
+        super(detailMessage);
+    }
+
+    public AppFuseMountException(String detailMessage, Throwable throwable) {
+        super(detailMessage, throwable);
+    }
+
+    /**
+     * Rethrow as a {@link RuntimeException} subclass that is handled by
+     * {@link Parcel#writeException(Exception)}.
+     */
+    public IllegalArgumentException rethrowAsParcelableException() {
+        throw new IllegalStateException(getMessage(), this);
+    }
+}
diff --git a/services/core/java/com/android/server/BluetoothAirplaneModeListener.java b/services/core/java/com/android/server/BluetoothAirplaneModeListener.java
index 197321f..263ff18 100644
--- a/services/core/java/com/android/server/BluetoothAirplaneModeListener.java
+++ b/services/core/java/com/android/server/BluetoothAirplaneModeListener.java
@@ -35,6 +35,7 @@
  * when Bluetooth is on and Bluetooth is in one of the following situations:
  *   1. Bluetooth A2DP is connected.
  *   2. Bluetooth Hearing Aid profile is connected.
+ *   3. Bluetooth LE Audio is connected
  */
 class BluetoothAirplaneModeListener {
     private static final String TAG = "BluetoothAirplaneModeListener";
@@ -132,7 +133,7 @@
             return false;
         }
         if (!mAirplaneHelper.isBluetoothOn() || !mAirplaneHelper.isAirplaneModeOn()
-                || !mAirplaneHelper.isA2dpOrHearingAidConnected()) {
+                || !mAirplaneHelper.isMediaProfileConnected()) {
             return false;
         }
         return true;
diff --git a/services/core/java/com/android/server/BluetoothManagerService.java b/services/core/java/com/android/server/BluetoothManagerService.java
index f62935a..8860a81 100644
--- a/services/core/java/com/android/server/BluetoothManagerService.java
+++ b/services/core/java/com/android/server/BluetoothManagerService.java
@@ -35,6 +35,7 @@
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothProtoEnums;
 import android.bluetooth.IBluetooth;
@@ -456,12 +457,13 @@
                     }
                 }
             } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)
-                    || BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+                    || BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(action)
+                    || BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED.equals(action)) {
                 final int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
                         BluetoothProfile.STATE_CONNECTED);
                 if (mHandler.hasMessages(MESSAGE_INIT_FLAGS_CHANGED)
                         && state == BluetoothProfile.STATE_DISCONNECTED
-                        && !mBluetoothModeChangeHelper.isA2dpOrHearingAidConnected()) {
+                        && !mBluetoothModeChangeHelper.isMediaProfileConnected()) {
                     Slog.i(TAG, "Device disconnected, reactivating pending flag changes");
                     onInitFlagsChanged();
                 }
@@ -2291,7 +2293,7 @@
                         Slog.d(TAG, "MESSAGE_INIT_FLAGS_CHANGED");
                     }
                     mHandler.removeMessages(MESSAGE_INIT_FLAGS_CHANGED);
-                    if (mBluetoothModeChangeHelper.isA2dpOrHearingAidConnected()) {
+                    if (mBluetoothModeChangeHelper.isMediaProfileConnected()) {
                         Slog.i(TAG, "Delaying MESSAGE_INIT_FLAGS_CHANGED by "
                                 + DELAY_FOR_RETRY_INIT_FLAG_CHECK_MS
                                 + " ms due to existing connections");
diff --git a/services/core/java/com/android/server/BluetoothModeChangeHelper.java b/services/core/java/com/android/server/BluetoothModeChangeHelper.java
index 3642e4d..e5854c9 100644
--- a/services/core/java/com/android/server/BluetoothModeChangeHelper.java
+++ b/services/core/java/com/android/server/BluetoothModeChangeHelper.java
@@ -20,6 +20,7 @@
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothProfile.ServiceListener;
 import android.content.Context;
@@ -37,6 +38,7 @@
 public class BluetoothModeChangeHelper {
     private volatile BluetoothA2dp mA2dp;
     private volatile BluetoothHearingAid mHearingAid;
+    private volatile BluetoothLeAudio mLeAudio;
     private final BluetoothAdapter mAdapter;
     private final Context mContext;
 
@@ -47,6 +49,7 @@
         mAdapter.getProfileProxy(mContext, mProfileServiceListener, BluetoothProfile.A2DP);
         mAdapter.getProfileProxy(mContext, mProfileServiceListener,
                 BluetoothProfile.HEARING_AID);
+        mAdapter.getProfileProxy(mContext, mProfileServiceListener, BluetoothProfile.LE_AUDIO);
     }
 
     private final ServiceListener mProfileServiceListener = new ServiceListener() {
@@ -60,6 +63,9 @@
                 case BluetoothProfile.HEARING_AID:
                     mHearingAid = (BluetoothHearingAid) proxy;
                     break;
+                case BluetoothProfile.LE_AUDIO:
+                    mLeAudio = (BluetoothLeAudio) proxy;
+                    break;
                 default:
                     break;
             }
@@ -75,6 +81,9 @@
                 case BluetoothProfile.HEARING_AID:
                     mHearingAid = null;
                     break;
+                case BluetoothProfile.LE_AUDIO:
+                    mLeAudio = null;
+                    break;
                 default:
                     break;
             }
@@ -82,8 +91,8 @@
     };
 
     @VisibleForTesting
-    public boolean isA2dpOrHearingAidConnected() {
-        return isA2dpConnected() || isHearingAidConnected();
+    public boolean isMediaProfileConnected() {
+        return isA2dpConnected() || isHearingAidConnected() || isLeAudioConnected();
     }
 
     @VisibleForTesting
@@ -142,4 +151,12 @@
         }
         return hearingAid.getConnectedDevices().size() > 0;
     }
+
+    private boolean isLeAudioConnected() {
+        final BluetoothLeAudio leAudio = mLeAudio;
+        if (leAudio == null) {
+            return false;
+        }
+        return leAudio.getConnectedDevices().size() > 0;
+    }
 }
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 69c2926..0e6e7df 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -1230,7 +1230,7 @@
             }
             for (int i = 0; i < mVolumes.size(); i++) {
                 final VolumeInfo vol = mVolumes.valueAt(i);
-                if (vol.isVisibleForRead(userId) && vol.isMountedReadable()) {
+                if (vol.isVisibleForUser(userId) && vol.isMountedReadable()) {
                     final StorageVolume userVol = vol.buildStorageVolume(mContext, userId, false);
                     mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget();
 
@@ -1558,13 +1558,13 @@
                     && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id)) {
                 Slog.v(TAG, "Found primary storage at " + vol);
                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
                 mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
 
             } else if (Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) {
                 Slog.v(TAG, "Found primary storage at " + vol);
                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
                 mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
             }
 
@@ -1574,13 +1574,13 @@
                     && vol.disk.isDefaultPrimary()) {
                 Slog.v(TAG, "Found primary storage at " + vol);
                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
             }
 
             // Adoptable public disks are visible to apps, since they meet
             // public API requirement of being in a stable location.
             if (vol.disk.isAdoptable()) {
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
             }
 
             vol.mountUserId = mCurrentUserId;
@@ -1591,7 +1591,7 @@
 
         } else if (vol.type == VolumeInfo.TYPE_STUB) {
             if (vol.disk.isStubVisible()) {
-                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
             }
             vol.mountUserId = mCurrentUserId;
             mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
@@ -1738,7 +1738,7 @@
                 // started after this point will trigger additional
                 // user-specific broadcasts.
                 for (int userId : mSystemUnlockedUsers) {
-                    if (vol.isVisibleForRead(userId)) {
+                    if (vol.isVisibleForUser(userId)) {
                         final StorageVolume userVol = vol.buildStorageVolume(mContext, userId,
                                 false);
                         mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget();
@@ -3560,24 +3560,24 @@
         }
 
         @Override
-        public ParcelFileDescriptor open() throws NativeDaemonConnectorException {
+        public ParcelFileDescriptor open() throws AppFuseMountException {
             try {
                 final FileDescriptor fd = mVold.mountAppFuse(uid, mountId);
                 mMounted = true;
                 return new ParcelFileDescriptor(fd);
             } catch (Exception e) {
-                throw new NativeDaemonConnectorException("Failed to mount", e);
+                throw new AppFuseMountException("Failed to mount", e);
             }
         }
 
         @Override
         public ParcelFileDescriptor openFile(int mountId, int fileId, int flags)
-                throws NativeDaemonConnectorException {
+                throws AppFuseMountException {
             try {
                 return new ParcelFileDescriptor(
                         mVold.openAppFuseFile(uid, mountId, fileId, flags));
             } catch (Exception e) {
-                throw new NativeDaemonConnectorException("Failed to open", e);
+                throw new AppFuseMountException("Failed to open", e);
             }
         }
 
@@ -3617,7 +3617,7 @@
                         // It seems the thread of mAppFuseBridge has already been terminated.
                         mAppFuseBridge = null;
                     }
-                } catch (NativeDaemonConnectorException e) {
+                } catch (AppFuseMountException e) {
                     throw e.rethrowAsParcelableException();
                 }
             }
@@ -3766,7 +3766,7 @@
                 if (forWrite) {
                     match = vol.isVisibleForWrite(userId);
                 } else {
-                    match = vol.isVisibleForRead(userId)
+                    match = vol.isVisibleForUser(userId)
                             || (includeInvisible && vol.getPath() != null);
                 }
                 if (!match) continue;
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index b068f86..0c990ec 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -141,7 +141,7 @@
  *                      |                     or its properties
  *                      v                              |
  * +-----------------------------------------------------------------------+
- * |                       UnderlyingNetworkTracker                        |
+ * |                       UnderlyingNetworkController                     |
  * |                                                                       |
  * | Manages lifecycle of underlying physical networks, filing requests to |
  * | bring them up, and releasing them as they become no longer necessary  |
diff --git a/services/core/java/com/android/server/net/IpConfigStore.java b/services/core/java/com/android/server/net/IpConfigStore.java
index df1eb6d..d17dbde 100644
--- a/services/core/java/com/android/server/net/IpConfigStore.java
+++ b/services/core/java/com/android/server/net/IpConfigStore.java
@@ -322,8 +322,11 @@
                                     gateway = InetAddresses.parseNumericAddress(in.readUTF());
                                 }
                                 // If the destination is a default IPv4 route, use the gateway
-                                // address unless already set.
-                                if (dest.getAddress() instanceof Inet4Address
+                                // address unless already set. If there is no destination, assume
+                                // it is default route and use the gateway address in all cases.
+                                if (dest == null) {
+                                    gatewayAddress = gateway;
+                                } else if (dest.getAddress() instanceof Inet4Address
                                         && dest.getPrefixLength() == 0 && gatewayAddress == null) {
                                     gatewayAddress = gateway;
                                 } else {
diff --git a/services/core/java/com/android/server/net/NetworkPolicyLogger.java b/services/core/java/com/android/server/net/NetworkPolicyLogger.java
index 654b17f..b45d87f 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyLogger.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyLogger.java
@@ -39,6 +39,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.RingBuffer;
 import com.android.server.am.ProcessList;
+import com.android.server.net.NetworkPolicyManagerService.UidBlockedState;
 
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
@@ -72,16 +73,6 @@
     private static final int EVENT_UPDATE_METERED_RESTRICTED_PKGS = 13;
     private static final int EVENT_APP_IDLE_WL_CHANGED = 14;
 
-    static final int NTWK_BLOCKED_POWER = 0;
-    static final int NTWK_ALLOWED_NON_METERED = 1;
-    static final int NTWK_BLOCKED_DENYLIST = 2;
-    static final int NTWK_ALLOWED_ALLOWLIST = 3;
-    static final int NTWK_ALLOWED_TMP_ALLOWLIST = 4;
-    static final int NTWK_BLOCKED_BG_RESTRICT = 5;
-    static final int NTWK_ALLOWED_DEFAULT = 6;
-    static final int NTWK_ALLOWED_SYSTEM = 7;
-    static final int NTWK_BLOCKED_RESTRICTED_MODE = 8;
-
     private final LogBuffer mNetworkBlockedBuffer = new LogBuffer(MAX_NETWORK_BLOCKED_LOG_SIZE);
     private final LogBuffer mUidStateChangeBuffer = new LogBuffer(MAX_LOG_SIZE);
     private final LogBuffer mEventsBuffer = new LogBuffer(MAX_LOG_SIZE);
@@ -90,12 +81,13 @@
 
     private final Object mLock = new Object();
 
-    void networkBlocked(int uid, int reason) {
+    void networkBlocked(int uid, UidBlockedState uidBlockedState) {
         synchronized (mLock) {
             if (LOGD || uid == mDebugUid) {
-                Slog.d(TAG, uid + " is " + getBlockedReason(reason));
+                Slog.d(TAG, "Blocked state of uid: " + uidBlockedState.toString());
             }
-            mNetworkBlockedBuffer.networkBlocked(uid, reason);
+            mNetworkBlockedBuffer.networkBlocked(uid, uidBlockedState.blockedReasons,
+                    uidBlockedState.allowedReasons, uidBlockedState.effectiveBlockedReasons);
         }
     }
 
@@ -269,29 +261,6 @@
         }
     }
 
-    private static String getBlockedReason(int reason) {
-        switch (reason) {
-            case NTWK_BLOCKED_POWER:
-                return "blocked by power restrictions";
-            case NTWK_ALLOWED_NON_METERED:
-                return "allowed on unmetered network";
-            case NTWK_BLOCKED_DENYLIST:
-                return "denylisted on metered network";
-            case NTWK_ALLOWED_ALLOWLIST:
-                return "allowlisted on metered network";
-            case NTWK_ALLOWED_TMP_ALLOWLIST:
-                return "temporary allowlisted on metered network";
-            case NTWK_BLOCKED_BG_RESTRICT:
-                return "blocked when background is restricted";
-            case NTWK_ALLOWED_DEFAULT:
-                return "allowed by default";
-            case NTWK_BLOCKED_RESTRICTED_MODE:
-                return "blocked by restricted networking mode";
-            default:
-                return String.valueOf(reason);
-        }
-    }
-
     private static String getPolicyChangedLog(int uid, int oldPolicy, int newPolicy) {
         return "Policy for " + uid + " changed from "
                 + NetworkPolicyManager.uidPoliciesToString(oldPolicy) + " to "
@@ -402,14 +371,17 @@
             data.timeStamp = System.currentTimeMillis();
         }
 
-        public void networkBlocked(int uid, int reason) {
+        public void networkBlocked(int uid, int blockedReasons, int allowedReasons,
+                int effectiveBlockedReasons) {
             final Data data = getNextSlot();
             if (data == null) return;
 
             data.reset();
             data.type = EVENT_NETWORK_BLOCKED;
             data.ifield1 = uid;
-            data.ifield2 = reason;
+            data.ifield2 = blockedReasons;
+            data.ifield3 = allowedReasons;
+            data.ifield4 = effectiveBlockedReasons;
             data.timeStamp = System.currentTimeMillis();
         }
 
@@ -554,7 +526,8 @@
                 case EVENT_TYPE_GENERIC:
                     return data.sfield1;
                 case EVENT_NETWORK_BLOCKED:
-                    return data.ifield1 + "-" + getBlockedReason(data.ifield2);
+                    return data.ifield1 + "-" + UidBlockedState.toString(
+                            data.ifield2, data.ifield3, data.ifield4);
                 case EVENT_UID_STATE_CHANGED:
                     return data.ifield1 + ":" + ProcessList.makeProcStateString(data.ifield2)
                             + ":" + ActivityManager.getCapabilitiesSummary(data.ifield3)
@@ -593,17 +566,18 @@
         }
     }
 
-    public final static class Data {
-        int type;
-        long timeStamp;
+    private static final class Data {
+        public int type;
+        public long timeStamp;
 
-        int ifield1;
-        int ifield2;
-        int ifield3;
-        long lfield1;
-        boolean bfield1;
-        boolean bfield2;
-        String sfield1;
+        public int ifield1;
+        public int ifield2;
+        public int ifield3;
+        public int ifield4;
+        public long lfield1;
+        public boolean bfield1;
+        public boolean bfield2;
+        public String sfield1;
 
         public void reset(){
             sfield1 = null;
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 20687c6..367f338 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -79,14 +79,10 @@
 import static android.net.NetworkPolicyManager.ALLOWED_REASON_SYSTEM;
 import static android.net.NetworkPolicyManager.EXTRA_NETWORK_TEMPLATE;
 import static android.net.NetworkPolicyManager.FIREWALL_RULE_DEFAULT;
-import static android.net.NetworkPolicyManager.MASK_ALL_NETWORKS;
-import static android.net.NetworkPolicyManager.MASK_METERED_NETWORKS;
-import static android.net.NetworkPolicyManager.MASK_RESTRICTED_MODE_NETWORKS;
 import static android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND;
 import static android.net.NetworkPolicyManager.POLICY_NONE;
 import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
 import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL;
-import static android.net.NetworkPolicyManager.RULE_ALLOW_METERED;
 import static android.net.NetworkPolicyManager.RULE_NONE;
 import static android.net.NetworkPolicyManager.RULE_REJECT_ALL;
 import static android.net.NetworkPolicyManager.RULE_REJECT_METERED;
@@ -135,15 +131,6 @@
 import static com.android.internal.util.XmlUtils.writeLongAttribute;
 import static com.android.internal.util.XmlUtils.writeStringAttribute;
 import static com.android.server.NetworkManagementService.LIMIT_GLOBAL_ALERT;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_ALLOWED_ALLOWLIST;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_ALLOWED_DEFAULT;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_ALLOWED_NON_METERED;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_ALLOWED_SYSTEM;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_ALLOWED_TMP_ALLOWLIST;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_BLOCKED_BG_RESTRICT;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_BLOCKED_DENYLIST;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_BLOCKED_POWER;
-import static com.android.server.net.NetworkPolicyLogger.NTWK_BLOCKED_RESTRICTED_MODE;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_UPDATED;
 
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
@@ -518,8 +505,6 @@
 
     /** Defined UID policies. */
     @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidPolicy = new SparseIntArray();
-    /** Currently derived rules for each UID. */
-    @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidRules = new SparseIntArray();
 
     @GuardedBy("mUidRulesFirstLock")
     final SparseIntArray mUidFirewallStandbyRules = new SparseIntArray();
@@ -598,6 +583,10 @@
     @GuardedBy("mUidRulesFirstLock")
     private final SparseArray<UidBlockedState> mUidBlockedState = new SparseArray<>();
 
+    /** Objects used temporarily while computing the new blocked state for each uid. */
+    @GuardedBy("mUidRulesFirstLock")
+    private final SparseArray<UidBlockedState> mTmpUidBlockedState = new SparseArray<>();
+
     /** Map from network ID to last observed meteredness state */
     @GuardedBy("mNetworkPoliciesSecondLock")
     private final SparseBooleanArray mNetworkMetered = new SparseBooleanArray();
@@ -3825,7 +3814,7 @@
 
                 final SparseBooleanArray knownUids = new SparseBooleanArray();
                 collectKeys(mUidState, knownUids);
-                collectKeys(mUidRules, knownUids);
+                collectKeys(mUidBlockedState, knownUids);
 
                 fout.println("Status for all known UIDs:");
                 fout.increaseIndent();
@@ -3843,23 +3832,13 @@
                         fout.print(uidState.toString());
                     }
 
-                    final int uidRules = mUidRules.get(uid, RULE_NONE);
-                    fout.print(" rules=");
-                    fout.print(uidRulesToString(uidRules));
-                    fout.println();
-                }
-                fout.decreaseIndent();
-
-                fout.println("Status for just UIDs with rules:");
-                fout.increaseIndent();
-                size = mUidRules.size();
-                for (int i = 0; i < size; i++) {
-                    final int uid = mUidRules.keyAt(i);
-                    fout.print("UID=");
-                    fout.print(uid);
-                    final int uidRules = mUidRules.get(uid, RULE_NONE);
-                    fout.print(" rules=");
-                    fout.print(uidRulesToString(uidRules));
+                    final UidBlockedState uidBlockedState = mUidBlockedState.get(uid);
+                    if (uidBlockedState == null) {
+                        fout.print(" blocked_state={null}");
+                    } else {
+                        fout.print(" blocked_state=");
+                        fout.print(uidBlockedState.toString());
+                    }
                     fout.println();
                 }
                 fout.decreaseIndent();
@@ -4010,22 +3989,17 @@
     void updateRestrictedModeAllowlistUL() {
         mUidFirewallRestrictedModeRules.clear();
         forEachUid("updateRestrictedModeAllowlist", uid -> {
-            final int oldUidRule = mUidRules.get(uid);
-            final int newUidRule = getNewRestrictedModeUidRule(uid, oldUidRule);
-            final boolean hasUidRuleChanged = oldUidRule != newUidRule;
-            final int newFirewallRule = getRestrictedModeFirewallRule(newUidRule);
+            synchronized (mUidRulesFirstLock) {
+                final UidBlockedState uidBlockedState = updateBlockedReasonsForRestrictedModeUL(
+                        uid);
+                final int newFirewallRule = getRestrictedModeFirewallRule(uidBlockedState);
 
-            // setUidFirewallRulesUL will allowlist all uids that are passed to it, so only add
-            // non-default rules.
-            if (newFirewallRule != FIREWALL_RULE_DEFAULT) {
-                mUidFirewallRestrictedModeRules.append(uid, newFirewallRule);
+                // setUidFirewallRulesUL will allowlist all uids that are passed to it, so only add
+                // non-default rules.
+                if (newFirewallRule != FIREWALL_RULE_DEFAULT) {
+                    mUidFirewallRestrictedModeRules.append(uid, newFirewallRule);
+                }
             }
-
-            if (hasUidRuleChanged) {
-                mUidRules.put(uid, newUidRule);
-                mHandler.obtainMessage(MSG_RULES_CHANGED, uid, newUidRule).sendToTarget();
-            }
-            updateBlockedReasonsForRestrictedModeUL(uid);
         });
         if (mRestrictedNetworkingMode) {
             // firewall rules only need to be set when this mode is being enabled.
@@ -4038,15 +4012,7 @@
     @VisibleForTesting
     @GuardedBy("mUidRulesFirstLock")
     void updateRestrictedModeForUidUL(int uid) {
-        final int oldUidRule = mUidRules.get(uid);
-        final int newUidRule = getNewRestrictedModeUidRule(uid, oldUidRule);
-        final boolean hasUidRuleChanged = oldUidRule != newUidRule;
-
-        if (hasUidRuleChanged) {
-            mUidRules.put(uid, newUidRule);
-            mHandler.obtainMessage(MSG_RULES_CHANGED, uid, newUidRule).sendToTarget();
-        }
-        updateBlockedReasonsForRestrictedModeUL(uid);
+        final UidBlockedState uidBlockedState = updateBlockedReasonsForRestrictedModeUL(uid);
 
         // if restricted networking mode is on, and the app has an access exemption, the uid rule
         // will not change, but the firewall rule will have to be updated.
@@ -4054,16 +4020,14 @@
             // Note: setUidFirewallRule also updates mUidFirewallRestrictedModeRules.
             // In this case, default firewall rules can also be added.
             setUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, uid,
-                    getRestrictedModeFirewallRule(newUidRule));
+                    getRestrictedModeFirewallRule(uidBlockedState));
         }
     }
 
-    private void updateBlockedReasonsForRestrictedModeUL(int uid) {
-        UidBlockedState uidBlockedState = mUidBlockedState.get(uid);
-        if (uidBlockedState == null) {
-            uidBlockedState = new UidBlockedState();
-            mUidBlockedState.put(uid, uidBlockedState);
-        }
+    @GuardedBy("mUidRulesFirstLock")
+    private UidBlockedState updateBlockedReasonsForRestrictedModeUL(int uid) {
+        final UidBlockedState uidBlockedState = getOrCreateUidBlockedStateForUid(
+                mUidBlockedState, uid);
         final int oldEffectiveBlockedReasons = uidBlockedState.effectiveBlockedReasons;
         if (mRestrictedNetworkingMode) {
             uidBlockedState.blockedReasons |= BLOCKED_REASON_RESTRICTED_MODE;
@@ -4077,23 +4041,16 @@
         }
         uidBlockedState.updateEffectiveBlockedReasons();
         if (oldEffectiveBlockedReasons != uidBlockedState.effectiveBlockedReasons) {
-            mHandler.obtainMessage(MSG_BLOCKED_REASON_CHANGED, uid,
-                    uidBlockedState.effectiveBlockedReasons, oldEffectiveBlockedReasons)
-                    .sendToTarget();
+            postBlockedReasonsChangedMsg(uid,
+                    uidBlockedState.effectiveBlockedReasons, oldEffectiveBlockedReasons);
+
+            postUidRulesChangedMsg(uid, uidBlockedState.deriveUidRules());
         }
+        return uidBlockedState;
     }
 
-    private int getNewRestrictedModeUidRule(int uid, int oldUidRule) {
-        int newRule = oldUidRule;
-        newRule &= ~MASK_RESTRICTED_MODE_NETWORKS;
-        if (mRestrictedNetworkingMode && !hasRestrictedModeAccess(uid)) {
-            newRule |= RULE_REJECT_RESTRICTED_MODE;
-        }
-        return newRule;
-    }
-
-    private static int getRestrictedModeFirewallRule(int uidRule) {
-        if ((uidRule & RULE_REJECT_RESTRICTED_MODE) != 0) {
+    private static int getRestrictedModeFirewallRule(UidBlockedState uidBlockedState) {
+        if ((uidBlockedState.effectiveBlockedReasons & BLOCKED_REASON_RESTRICTED_MODE) != 0) {
             // rejected in restricted mode, this is the default behavior.
             return FIREWALL_RULE_DEFAULT;
         } else {
@@ -4301,16 +4258,12 @@
             if (!isUidValidForDenylistRulesUL(uid)) {
                 continue;
             }
-            int oldRules = mUidRules.get(uid);
-            if (enableChain) {
-                // Chain wasn't enabled before and the other power-related
-                // chains are allowlists, so we can clear the
-                // MASK_ALL_NETWORKS part of the rules and re-inform listeners if
-                // the effective rules result in blocking network access.
-                oldRules &= MASK_METERED_NETWORKS;
-            } else {
-                // Skip if it had no restrictions to begin with
-                if ((oldRules & MASK_ALL_NETWORKS) == 0) continue;
+            final UidBlockedState uidBlockedState = getOrCreateUidBlockedStateForUid(
+                    mUidBlockedState, uid);
+            if (!enableChain && (uidBlockedState.blockedReasons & ~BLOCKED_METERED_REASON_MASK)
+                    == BLOCKED_REASON_NONE) {
+                // Chain isn't enabled and the uid had no restrictions to begin with.
+                continue;
             }
             final boolean isUidIdle = !paroled && isUidIdle(uid);
             if (isUidIdle && !mPowerSaveTempWhitelistAppIds.get(UserHandle.getAppId(uid))
@@ -4320,13 +4273,7 @@
             } else {
                 mUidFirewallStandbyRules.put(uid, FIREWALL_RULE_DEFAULT);
             }
-            final int newUidRules = updateRulesForPowerRestrictionsUL(uid, oldRules,
-                    isUidIdle);
-            if (newUidRules == RULE_NONE) {
-                mUidRules.delete(uid);
-            } else {
-                mUidRules.put(uid, newUidRules);
-            }
+            updateRulesForPowerRestrictionsUL(uid, isUidIdle);
         }
         setUidFirewallRulesUL(FIREWALL_CHAIN_STANDBY, blockedUids,
                 enableChain ? CHAIN_TOGGLE_ENABLE : CHAIN_TOGGLE_DISABLE);
@@ -4544,6 +4491,7 @@
             mInternetPermissionMap.put(uid, hasPermission);
             return hasPermission;
         } catch (RemoteException e) {
+            // ignored; service lives in system_server
         }
         return true;
     }
@@ -4554,7 +4502,7 @@
     @GuardedBy("mUidRulesFirstLock")
     private void onUidDeletedUL(int uid) {
         // First cleanup in-memory state synchronously...
-        mUidRules.delete(uid);
+        mUidBlockedState.delete(uid);
         mUidPolicy.delete(uid);
         mUidFirewallStandbyRules.delete(uid);
         mUidFirewallDozableRules.delete(uid);
@@ -4640,7 +4588,7 @@
      * permission, since there is no need to change the {@code iptables} rule if the app does not
      * have permission to use the internet.
      *
-     * <p>The {@link #mUidRules} map is used to define the transtion of states of an UID.
+     * <p>The {@link #mUidBlockedState} map is used to define the transition of states of an UID.
      *
      */
     private void updateRulesForDataUsageRestrictionsUL(int uid) {
@@ -4655,6 +4603,7 @@
         }
     }
 
+    @GuardedBy("mUidRulesFirstLock")
     private void updateRulesForDataUsageRestrictionsULInner(int uid) {
         if (!isUidValidForAllowlistRulesUL(uid)) {
             if (LOGD) Slog.d(TAG, "no need to update restrict data rules for uid " + uid);
@@ -4662,38 +4611,17 @@
         }
 
         final int uidPolicy = mUidPolicy.get(uid, POLICY_NONE);
-        final int oldUidRules = mUidRules.get(uid, RULE_NONE);
         final boolean isForeground = isUidForegroundOnRestrictBackgroundUL(uid);
         final boolean isRestrictedByAdmin = isRestrictedByAdminUL(uid);
-        UidBlockedState uidBlockedState = mUidBlockedState.get(uid);
-        if (uidBlockedState == null) {
-            uidBlockedState = new UidBlockedState();
-            mUidBlockedState.put(uid, uidBlockedState);
-        }
+        final UidBlockedState uidBlockedState = getOrCreateUidBlockedStateForUid(
+                mUidBlockedState, uid);
+        final UidBlockedState previousUidBlockedState = getOrCreateUidBlockedStateForUid(
+                mTmpUidBlockedState, uid);
+        previousUidBlockedState.copyFrom(uidBlockedState);
 
         final boolean isDenied = (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0;
         final boolean isAllowed = (uidPolicy & POLICY_ALLOW_METERED_BACKGROUND) != 0;
 
-        // copy oldUidRules and clear out METERED_NETWORKS rules.
-        int newUidRules = oldUidRules & (~MASK_METERED_NETWORKS);
-
-        // First step: define the new rule based on user restrictions and foreground state.
-        if (isRestrictedByAdmin) {
-            newUidRules |= RULE_REJECT_METERED;
-        } else if (isForeground) {
-            if (isDenied || (mRestrictBackground && !isAllowed)) {
-                newUidRules |= RULE_TEMPORARY_ALLOW_METERED;
-            } else if (isAllowed) {
-                newUidRules |= RULE_ALLOW_METERED;
-            }
-        } else {
-            if (isDenied) {
-                newUidRules |= RULE_REJECT_METERED;
-            } else if (mRestrictBackground && isAllowed) {
-                newUidRules |= RULE_ALLOW_METERED;
-            }
-        }
-
         int newBlockedReasons = BLOCKED_REASON_NONE;
         int newAllowedReasons = ALLOWED_REASON_NONE;
         newBlockedReasons |= (isRestrictedByAdmin ? BLOCKED_METERED_REASON_ADMIN_DISABLED : 0);
@@ -4704,16 +4632,48 @@
         newAllowedReasons |= (isForeground ? ALLOWED_METERED_REASON_FOREGROUND : 0);
         newAllowedReasons |= (isAllowed ? ALLOWED_METERED_REASON_USER_EXEMPTED : 0);
 
+        uidBlockedState.blockedReasons = (uidBlockedState.blockedReasons
+                & ~BLOCKED_METERED_REASON_MASK) | newBlockedReasons;
+        uidBlockedState.allowedReasons = (uidBlockedState.allowedReasons
+                & ~ALLOWED_METERED_REASON_MASK) | newAllowedReasons;
+        uidBlockedState.updateEffectiveBlockedReasons();
+        final int oldEffectiveBlockedReasons = previousUidBlockedState.effectiveBlockedReasons;
+        final int newEffectiveBlockedReasons = uidBlockedState.effectiveBlockedReasons;
+        if (oldEffectiveBlockedReasons != newEffectiveBlockedReasons) {
+            postBlockedReasonsChangedMsg(uid,
+                    newEffectiveBlockedReasons, oldEffectiveBlockedReasons);
+
+            postUidRulesChangedMsg(uid, uidBlockedState.deriveUidRules());
+        }
+
+        // Note that the conditionals below are for avoiding unnecessary calls to netd.
+        // TODO: Measure the performance for doing a no-op call to netd so that we can
+        // remove the conditionals to simplify the logic below. We can also further reduce
+        // some calls to netd if they turn out to be costly.
+        final int denylistReasons = BLOCKED_METERED_REASON_ADMIN_DISABLED
+                | BLOCKED_METERED_REASON_USER_RESTRICTED;
+        if ((oldEffectiveBlockedReasons & denylistReasons) != BLOCKED_REASON_NONE
+                || (newEffectiveBlockedReasons & denylistReasons) != BLOCKED_REASON_NONE) {
+            setMeteredNetworkDenylist(uid,
+                    (newEffectiveBlockedReasons & denylistReasons) != BLOCKED_REASON_NONE);
+        }
+        final int allowlistReasons = ALLOWED_METERED_REASON_FOREGROUND
+                | ALLOWED_METERED_REASON_USER_EXEMPTED;
+        final int oldAllowedReasons = previousUidBlockedState.allowedReasons;
+        if ((oldAllowedReasons & allowlistReasons) != ALLOWED_REASON_NONE
+                || (newAllowedReasons & allowlistReasons) != ALLOWED_REASON_NONE) {
+            setMeteredNetworkAllowlist(uid,
+                    (newAllowedReasons & allowlistReasons) != ALLOWED_REASON_NONE);
+        }
+
         if (LOGV) {
             Log.v(TAG, "updateRuleForRestrictBackgroundUL(" + uid + ")"
                     + ": isForeground=" +isForeground
                     + ", isDenied=" + isDenied
                     + ", isAllowed=" + isAllowed
                     + ", isRestrictedByAdmin=" + isRestrictedByAdmin
-                    + ", oldRule=" + uidRulesToString(oldUidRules & MASK_METERED_NETWORKS)
-                    + ", newRule=" + uidRulesToString(newUidRules & MASK_METERED_NETWORKS)
-                    + ", newUidRules=" + uidRulesToString(newUidRules)
-                    + ", oldUidRules=" + uidRulesToString(oldUidRules)
+                    + ", oldBlockedState=" + previousUidBlockedState.toString()
+                    + ", newBlockedState="
                     + ", oldBlockedMeteredReasons=" + NetworkPolicyManager.blockedReasonsToString(
                     uidBlockedState.blockedReasons & BLOCKED_METERED_REASON_MASK)
                     + ", oldBlockedMeteredEffectiveReasons="
@@ -4722,84 +4682,11 @@
                     + ", oldAllowedMeteredReasons=" + NetworkPolicyManager.blockedReasonsToString(
                     uidBlockedState.allowedReasons & BLOCKED_METERED_REASON_MASK));
         }
-
-        if (newUidRules == RULE_NONE) {
-            mUidRules.delete(uid);
-        } else {
-            mUidRules.put(uid, newUidRules);
-        }
-
-        // Second step: apply bw changes based on change of state.
-        if (newUidRules != oldUidRules) {
-            if (hasRule(newUidRules, RULE_TEMPORARY_ALLOW_METERED)) {
-                // Temporarily allow foreground app, removing from denylist if necessary
-                // (since bw_penalty_box prevails over bw_happy_box).
-
-                setMeteredNetworkAllowlist(uid, true);
-                // TODO: if statement below is used to avoid an unnecessary call to netd / iptables,
-                // but ideally it should be just:
-                //    setMeteredNetworkDenylist(uid, isDenied);
-                if (isDenied) {
-                    setMeteredNetworkDenylist(uid, false);
-                }
-            } else if (hasRule(oldUidRules, RULE_TEMPORARY_ALLOW_METERED)) {
-                // Remove temporary exemption from app that is not on foreground anymore.
-
-                // TODO: if statements below are used to avoid unnecessary calls to netd / iptables,
-                // but ideally they should be just:
-                //    setMeteredNetworkAllowlist(uid, isAllowed);
-                //    setMeteredNetworkDenylist(uid, isDenied);
-                if (!isAllowed) {
-                    setMeteredNetworkAllowlist(uid, false);
-                }
-                if (isDenied || isRestrictedByAdmin) {
-                    setMeteredNetworkDenylist(uid, true);
-                }
-            } else if (hasRule(newUidRules, RULE_REJECT_METERED)
-                    || hasRule(oldUidRules, RULE_REJECT_METERED)) {
-                // Flip state because app was explicitly added or removed to denylist.
-                setMeteredNetworkDenylist(uid, (isDenied || isRestrictedByAdmin));
-                if (hasRule(oldUidRules, RULE_REJECT_METERED) && isAllowed) {
-                    // Since denial prevails over allowance, we need to handle the special case
-                    // where app is allowed and denied at the same time (although such
-                    // scenario should be blocked by the UI), then it is removed from the denylist.
-                    setMeteredNetworkAllowlist(uid, isAllowed);
-                }
-            } else if (hasRule(newUidRules, RULE_ALLOW_METERED)
-                    || hasRule(oldUidRules, RULE_ALLOW_METERED)) {
-                // Flip state because app was explicitly added or removed to allowlist.
-                setMeteredNetworkAllowlist(uid, isAllowed);
-            } else {
-                // All scenarios should have been covered above.
-                Log.wtf(TAG, "Unexpected change of metered UID state for " + uid
-                        + ": foreground=" + isForeground
-                        + ", allowlisted=" + isAllowed
-                        + ", denylisted=" + isDenied
-                        + ", isRestrictedByAdmin=" + isRestrictedByAdmin
-                        + ", newRule=" + uidRulesToString(newUidRules)
-                        + ", oldRule=" + uidRulesToString(oldUidRules));
-            }
-
-            // Dispatch changed rule to existing listeners.
-            mHandler.obtainMessage(MSG_RULES_CHANGED, uid, newUidRules).sendToTarget();
-        }
-
-        final int oldEffectiveBlockedReasons = uidBlockedState.effectiveBlockedReasons;
-        uidBlockedState.blockedReasons = (uidBlockedState.blockedReasons
-                & ~BLOCKED_METERED_REASON_MASK) | newBlockedReasons;
-        uidBlockedState.allowedReasons = (uidBlockedState.allowedReasons
-                & ~ALLOWED_METERED_REASON_MASK) | newAllowedReasons;
-        uidBlockedState.updateEffectiveBlockedReasons();
-        if (oldEffectiveBlockedReasons != uidBlockedState.effectiveBlockedReasons) {
-            mHandler.obtainMessage(MSG_BLOCKED_REASON_CHANGED, uid,
-                    uidBlockedState.effectiveBlockedReasons, oldEffectiveBlockedReasons)
-                    .sendToTarget();
-        }
     }
 
     /**
-     * Updates the power-related part of the {@link #mUidRules} for a given map, and notify external
-     * listeners in case of change.
+     * Updates the power-related part of the {@link #mUidBlockedState} for a given map, and
+     * notify external listeners in case of change.
      * <p>
      * There are 3 power-related rules that affects whether an app has background access on
      * non-metered networks, and when the condition applies and the UID is not allowed for power
@@ -4810,23 +4697,15 @@
      * <li>Battery Saver Mode is on: {@code fw_powersave} firewall chain.
      * </ul>
      * <p>
-     * This method updates the power-related part of the {@link #mUidRules} for a given uid based on
-     * these modes, the UID process state (foreground or not), and the UID allowlist state.
+     * This method updates the power-related part of the {@link #mUidBlockedState} for a given
+     * uid based on these modes, the UID process state (foreground or not), and the UID
+     * allowlist state.
      * <p>
      * <strong>NOTE: </strong>This method does not update the firewall rules on {@code netd}.
      */
     @GuardedBy("mUidRulesFirstLock")
     private void updateRulesForPowerRestrictionsUL(int uid) {
-        final int oldUidRules = mUidRules.get(uid, RULE_NONE);
-
-        final int newUidRules = updateRulesForPowerRestrictionsUL(uid, oldUidRules,
-                isUidIdle(uid));
-
-        if (newUidRules == RULE_NONE) {
-            mUidRules.delete(uid);
-        } else {
-            mUidRules.put(uid, newUidRules);
-        }
+        updateRulesForPowerRestrictionsUL(uid, isUidIdle(uid));
     }
 
     /**
@@ -4835,56 +4714,37 @@
      * @param uid the uid of the app to update rules for
      * @param oldUidRules the current rules for the uid, in order to determine if there's a change
      * @param isUidIdle whether uid is idle or not
-     *
-     * @return the new computed rules for the uid
      */
     @GuardedBy("mUidRulesFirstLock")
-    private int updateRulesForPowerRestrictionsUL(int uid, int oldUidRules, boolean isUidIdle) {
+    private void updateRulesForPowerRestrictionsUL(int uid, boolean isUidIdle) {
         if (Trace.isTagEnabled(Trace.TRACE_TAG_NETWORK)) {
             Trace.traceBegin(Trace.TRACE_TAG_NETWORK,
-                    "updateRulesForPowerRestrictionsUL: " + uid + "/" + oldUidRules + "/"
+                    "updateRulesForPowerRestrictionsUL: " + uid + "/"
                             + (isUidIdle ? "I" : "-"));
         }
         try {
-            return updateRulesForPowerRestrictionsULInner(uid, oldUidRules, isUidIdle);
+            updateRulesForPowerRestrictionsULInner(uid, isUidIdle);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_NETWORK);
         }
     }
 
     @GuardedBy("mUidRulesFirstLock")
-    private int updateRulesForPowerRestrictionsULInner(int uid, int oldUidRules,
-            boolean isUidIdle) {
+    private void updateRulesForPowerRestrictionsULInner(int uid, boolean isUidIdle) {
         if (!isUidValidForDenylistRulesUL(uid)) {
             if (LOGD) Slog.d(TAG, "no need to update restrict power rules for uid " + uid);
-            return RULE_NONE;
+            return;
         }
 
-        final boolean restrictMode = isUidIdle || mRestrictPower || mDeviceIdleMode;
         final boolean isForeground = isUidForegroundOnRestrictPowerUL(uid);
 
         final boolean isWhitelisted = isWhitelistedFromPowerSaveUL(uid, mDeviceIdleMode);
 
-        // Copy existing uid rules and clear ALL_NETWORK rules.
-        int newUidRules = oldUidRules & (~MASK_ALL_NETWORKS);
-
-        UidBlockedState uidBlockedState = mUidBlockedState.get(uid);
-        if (uidBlockedState == null) {
-            uidBlockedState = new UidBlockedState();
-            mUidBlockedState.put(uid, uidBlockedState);
-        }
-
-        // First step: define the new rule based on user restrictions and foreground state.
-
-        // NOTE: if statements below could be inlined, but it's easier to understand the logic
-        // by considering the foreground and non-foreground states.
-        if (isForeground) {
-            if (restrictMode) {
-                newUidRules |= RULE_ALLOW_ALL;
-            }
-        } else if (restrictMode) {
-            newUidRules |= isWhitelisted ? RULE_ALLOW_ALL : RULE_REJECT_ALL;
-        }
+        final UidBlockedState uidBlockedState = getOrCreateUidBlockedStateForUid(
+                mUidBlockedState, uid);
+        final UidBlockedState previousUidBlockedState = getOrCreateUidBlockedStateForUid(
+                mTmpUidBlockedState, uid);
+        previousUidBlockedState.copyFrom(uidBlockedState);
 
         int newBlockedReasons = BLOCKED_REASON_NONE;
         int newAllowedReasons = ALLOWED_REASON_NONE;
@@ -4900,6 +4760,20 @@
         newAllowedReasons |= (isWhitelistedFromPowerSaveExceptIdleUL(uid)
                 ? ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST : 0);
 
+        uidBlockedState.blockedReasons = (uidBlockedState.blockedReasons
+                & BLOCKED_METERED_REASON_MASK) | newBlockedReasons;
+        uidBlockedState.allowedReasons = (uidBlockedState.allowedReasons
+                & ALLOWED_METERED_REASON_MASK) | newAllowedReasons;
+        uidBlockedState.updateEffectiveBlockedReasons();
+        if (previousUidBlockedState.effectiveBlockedReasons
+                != uidBlockedState.effectiveBlockedReasons) {
+            postBlockedReasonsChangedMsg(uid,
+                    uidBlockedState.effectiveBlockedReasons,
+                    previousUidBlockedState.effectiveBlockedReasons);
+
+            postUidRulesChangedMsg(uid, uidBlockedState.deriveUidRules());
+        }
+
         if (LOGV) {
             Log.v(TAG, "updateRulesForPowerRestrictionsUL(" + uid + ")"
                     + ", isIdle: " + isUidIdle
@@ -4907,43 +4781,9 @@
                     + ", mDeviceIdleMode: " + mDeviceIdleMode
                     + ", isForeground=" + isForeground
                     + ", isWhitelisted=" + isWhitelisted
-                    + ", oldRule=" + uidRulesToString(oldUidRules & MASK_ALL_NETWORKS)
-                    + ", newRule=" + uidRulesToString(newUidRules & MASK_ALL_NETWORKS)
-                    + ", newUidRules=" + uidRulesToString(newUidRules)
-                    + ", oldUidRules=" + uidRulesToString(oldUidRules));
+                    + ", oldUidBlockedState=" + previousUidBlockedState.toString()
+                    + ", newUidBlockedState=" + uidBlockedState.toString());
         }
-
-        // Second step: notify listeners if state changed.
-        if (newUidRules != oldUidRules) {
-            if ((newUidRules & MASK_ALL_NETWORKS) == RULE_NONE || hasRule(newUidRules,
-                    RULE_ALLOW_ALL)) {
-                if (LOGV) Log.v(TAG, "Allowing non-metered access for UID " + uid);
-            } else if (hasRule(newUidRules, RULE_REJECT_ALL)) {
-                if (LOGV) Log.v(TAG, "Rejecting non-metered access for UID " + uid);
-            } else {
-                // All scenarios should have been covered above
-                Log.wtf(TAG, "Unexpected change of non-metered UID state for " + uid
-                        + ": foreground=" + isForeground
-                        + ", whitelisted=" + isWhitelisted
-                        + ", newRule=" + uidRulesToString(newUidRules)
-                        + ", oldRule=" + uidRulesToString(oldUidRules));
-            }
-            mHandler.obtainMessage(MSG_RULES_CHANGED, uid, newUidRules).sendToTarget();
-        }
-
-        final int oldEffectiveBlockedReasons = uidBlockedState.effectiveBlockedReasons;
-        uidBlockedState.blockedReasons = (uidBlockedState.blockedReasons
-                & BLOCKED_METERED_REASON_MASK) | newBlockedReasons;
-        uidBlockedState.allowedReasons = (uidBlockedState.allowedReasons
-                & ALLOWED_METERED_REASON_MASK) | newAllowedReasons;
-        uidBlockedState.updateEffectiveBlockedReasons();
-        if (oldEffectiveBlockedReasons != uidBlockedState.effectiveBlockedReasons) {
-            mHandler.obtainMessage(MSG_BLOCKED_REASON_CHANGED, uid,
-                    uidBlockedState.effectiveBlockedReasons, oldEffectiveBlockedReasons)
-                    .sendToTarget();
-        }
-
-        return newUidRules;
     }
 
     private class NetPolicyAppIdleStateChangeListener extends AppIdleStateChangeListener {
@@ -4971,10 +4811,23 @@
         }
     }
 
+    private void postBlockedReasonsChangedMsg(int uid, int newEffectiveBlockedReasons,
+            int oldEffectiveBlockedReasons) {
+        mHandler.obtainMessage(MSG_BLOCKED_REASON_CHANGED, uid,
+                newEffectiveBlockedReasons, oldEffectiveBlockedReasons)
+                .sendToTarget();
+    }
+
+    private void postUidRulesChangedMsg(int uid, int uidRules) {
+        mHandler.obtainMessage(MSG_RULES_CHANGED, uid, uidRules)
+                .sendToTarget();
+    }
+
     private void dispatchUidRulesChanged(INetworkPolicyListener listener, int uid, int uidRules) {
         try {
             listener.onUidRulesChanged(uid, uidRules);
         } catch (RemoteException ignored) {
+            // Ignore if there is an error sending the callback to the client.
         }
     }
 
@@ -4983,6 +4836,7 @@
         try {
             listener.onMeteredIfacesChanged(meteredIfaces);
         } catch (RemoteException ignored) {
+            // Ignore if there is an error sending the callback to the client.
         }
     }
 
@@ -4991,6 +4845,7 @@
         try {
             listener.onRestrictBackgroundChanged(restrictBackground);
         } catch (RemoteException ignored) {
+            // Ignore if there is an error sending the callback to the client.
         }
     }
 
@@ -4999,6 +4854,7 @@
         try {
             listener.onUidPoliciesChanged(uid, uidPolicies);
         } catch (RemoteException ignored) {
+            // Ignore if there is an error sending the callback to the client.
         }
     }
 
@@ -5007,6 +4863,7 @@
         try {
             listener.onSubscriptionOverride(subId, overrideMask, overrideValue, networkTypes);
         } catch (RemoteException ignored) {
+            // Ignore if there is an error sending the callback to the client.
         }
     }
 
@@ -5015,6 +4872,7 @@
         try {
             listener.onSubscriptionPlansChanged(subId, plans);
         } catch (RemoteException ignored) {
+            // Ignore if there is an error sending the callback to the client.
         }
     }
 
@@ -5023,6 +4881,7 @@
         try {
             listener.onBlockedReasonChanged(uid, oldBlockedReasons, newBlockedReasons);
         } catch (RemoteException ignored) {
+            // Ignore if there is an error sending the callback to the client.
         }
     }
 
@@ -5033,6 +4892,10 @@
                 case MSG_RULES_CHANGED: {
                     final int uid = msg.arg1;
                     final int uidRules = msg.arg2;
+                    if (LOGV) {
+                        Slog.v(TAG, "Dispatching rules=" + uidRulesToString(uidRules)
+                                + " for uid=" + uid);
+                    }
                     final int length = mListeners.beginBroadcast();
                     for (int i = 0; i < length; i++) {
                         final INetworkPolicyListener listener = mListeners.getBroadcastItem(i);
@@ -5605,7 +5468,7 @@
         }
     }
 
-    private static void collectKeys(SparseArray<UidState> source, SparseBooleanArray target) {
+    private static <T> void collectKeys(SparseArray<T> source, SparseBooleanArray target) {
         final int size = source.size();
         for (int i = 0; i < size; i++) {
             target.put(source.keyAt(i), true);
@@ -5653,90 +5516,38 @@
         final long startTime = mStatLogger.getTime();
 
         mContext.enforceCallingOrSelfPermission(OBSERVE_NETWORK_POLICY, TAG);
-        final int uidRules;
-        final boolean isBackgroundRestricted;
+        int blockedReasons;
         synchronized (mUidRulesFirstLock) {
-            uidRules = mUidRules.get(uid, RULE_NONE);
-            isBackgroundRestricted = mRestrictBackground;
+            final UidBlockedState uidBlockedState = mUidBlockedState.get(uid);
+            blockedReasons = uidBlockedState == null
+                    ? BLOCKED_REASON_NONE : uidBlockedState.effectiveBlockedReasons;
+            if (!isNetworkMetered) {
+                blockedReasons &= ~BLOCKED_METERED_REASON_MASK;
+            }
+            mLogger.networkBlocked(uid, uidBlockedState);
         }
-        final boolean ret = isUidNetworkingBlockedInternal(uid, uidRules, isNetworkMetered,
-                isBackgroundRestricted, mLogger);
 
         mStatLogger.logDurationStat(Stats.IS_UID_NETWORKING_BLOCKED, startTime);
 
-        return ret;
+        return blockedReasons != BLOCKED_REASON_NONE;
     }
 
     @Override
     public boolean isUidRestrictedOnMeteredNetworks(int uid) {
         mContext.enforceCallingOrSelfPermission(OBSERVE_NETWORK_POLICY, TAG);
-        final int uidRules;
-        final boolean isBackgroundRestricted;
         synchronized (mUidRulesFirstLock) {
-            uidRules = mUidRules.get(uid, RULE_ALLOW_ALL);
-            isBackgroundRestricted = mRestrictBackground;
+            final UidBlockedState uidBlockedState = mUidBlockedState.get(uid);
+            int blockedReasons = uidBlockedState == null
+                    ? BLOCKED_REASON_NONE : uidBlockedState.effectiveBlockedReasons;
+            blockedReasons &= BLOCKED_METERED_REASON_MASK;
+            return blockedReasons != BLOCKED_REASON_NONE;
         }
-        // TODO(b/177490332): The logic here might not be correct because it doesn't consider
-        //  RULE_REJECT_METERED condition. And it could be replaced by
-        //  isUidNetworkingBlockedInternal().
-        return isBackgroundRestricted
-                && !hasRule(uidRules, RULE_ALLOW_METERED)
-                && !hasRule(uidRules, RULE_TEMPORARY_ALLOW_METERED);
     }
 
     private static boolean isSystem(int uid) {
         return uid < Process.FIRST_APPLICATION_UID;
     }
 
-    static boolean isUidNetworkingBlockedInternal(int uid, int uidRules, boolean isNetworkMetered,
-            boolean isBackgroundRestricted, @Nullable NetworkPolicyLogger logger) {
-        final int reason;
-        // Networks are never blocked for system components
-        if (isSystem(uid)) {
-            reason = NTWK_ALLOWED_SYSTEM;
-        } else if (hasRule(uidRules, RULE_REJECT_RESTRICTED_MODE)) {
-            reason = NTWK_BLOCKED_RESTRICTED_MODE;
-        } else if (hasRule(uidRules, RULE_REJECT_ALL)) {
-            reason = NTWK_BLOCKED_POWER;
-        } else if (!isNetworkMetered) {
-            reason = NTWK_ALLOWED_NON_METERED;
-        } else if (hasRule(uidRules, RULE_REJECT_METERED)) {
-            reason = NTWK_BLOCKED_DENYLIST;
-        } else if (hasRule(uidRules, RULE_ALLOW_METERED)) {
-            reason = NTWK_ALLOWED_ALLOWLIST;
-        } else if (hasRule(uidRules, RULE_TEMPORARY_ALLOW_METERED)) {
-            reason = NTWK_ALLOWED_TMP_ALLOWLIST;
-        } else if (isBackgroundRestricted) {
-            reason = NTWK_BLOCKED_BG_RESTRICT;
-        } else {
-            reason = NTWK_ALLOWED_DEFAULT;
-        }
-
-        final boolean blocked;
-        switch(reason) {
-            case NTWK_ALLOWED_DEFAULT:
-            case NTWK_ALLOWED_NON_METERED:
-            case NTWK_ALLOWED_TMP_ALLOWLIST:
-            case NTWK_ALLOWED_ALLOWLIST:
-            case NTWK_ALLOWED_SYSTEM:
-                blocked = false;
-                break;
-            case NTWK_BLOCKED_RESTRICTED_MODE:
-            case NTWK_BLOCKED_POWER:
-            case NTWK_BLOCKED_DENYLIST:
-            case NTWK_BLOCKED_BG_RESTRICT:
-                blocked = true;
-                break;
-            default:
-                throw new IllegalArgumentException();
-        }
-        if (logger != null) {
-            logger.networkBlocked(uid, reason);
-        }
-
-        return blocked;
-    }
-
     private class NetworkPolicyManagerInternalImpl extends NetworkPolicyManagerInternal {
 
         @Override
@@ -5945,6 +5756,16 @@
         return (bundle != null) ? bundle.getBoolean(key, defaultValue) : defaultValue;
     }
 
+    private static UidBlockedState getOrCreateUidBlockedStateForUid(
+            SparseArray<UidBlockedState> uidBlockedStates, int uid) {
+        UidBlockedState uidBlockedState = uidBlockedStates.get(uid);
+        if (uidBlockedState == null) {
+            uidBlockedState = new UidBlockedState();
+            uidBlockedStates.put(uid, uidBlockedState);
+        }
+        return uidBlockedState;
+    }
+
     @VisibleForTesting
     static final class UidBlockedState {
         public int blockedReasons;
@@ -6008,9 +5829,180 @@
             }
             return effectiveBlockedReasons;
         }
+
+        @Override
+        public String toString() {
+            return toString(blockedReasons, allowedReasons, effectiveBlockedReasons);
+        }
+
+        public static String toString(int blockedReasons, int allowedReasons,
+                int effectiveBlockedReasons) {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            sb.append("blocked=").append(blockedReasonsToString(blockedReasons)).append(",");
+            sb.append("allowed=").append(allowedReasonsToString(allowedReasons)).append(",");
+            sb.append("effective=").append(blockedReasonsToString(effectiveBlockedReasons));
+            sb.append("}");
+            return sb.toString();
+        }
+
+        private static final int[] BLOCKED_REASONS = {
+                BLOCKED_REASON_BATTERY_SAVER,
+                BLOCKED_REASON_DOZE,
+                BLOCKED_REASON_APP_STANDBY,
+                BLOCKED_REASON_RESTRICTED_MODE,
+                BLOCKED_METERED_REASON_DATA_SAVER,
+                BLOCKED_METERED_REASON_USER_RESTRICTED,
+                BLOCKED_METERED_REASON_ADMIN_DISABLED,
+        };
+
+        private static final int[] ALLOWED_REASONS = {
+                ALLOWED_REASON_SYSTEM,
+                ALLOWED_REASON_FOREGROUND,
+                ALLOWED_REASON_POWER_SAVE_ALLOWLIST,
+                ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST,
+                ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS,
+                ALLOWED_METERED_REASON_USER_EXEMPTED,
+                ALLOWED_METERED_REASON_SYSTEM,
+                ALLOWED_METERED_REASON_FOREGROUND,
+        };
+
+        private static String blockedReasonToString(int blockedReason) {
+            switch (blockedReason) {
+                case BLOCKED_REASON_NONE:
+                    return "NONE";
+                case BLOCKED_REASON_BATTERY_SAVER:
+                    return "BATTERY_SAVER";
+                case BLOCKED_REASON_DOZE:
+                    return "DOZE";
+                case BLOCKED_REASON_APP_STANDBY:
+                    return "APP_STANDBY";
+                case BLOCKED_REASON_RESTRICTED_MODE:
+                    return "RESTRICTED_MODE";
+                case BLOCKED_METERED_REASON_DATA_SAVER:
+                    return "DATA_SAVER";
+                case BLOCKED_METERED_REASON_USER_RESTRICTED:
+                    return "METERED_USER_RESTRICTED";
+                case BLOCKED_METERED_REASON_ADMIN_DISABLED:
+                    return "METERED_ADMIN_DISABLED";
+                default:
+                    Slog.wtfStack(TAG, "Unknown blockedReason: " + blockedReason);
+                    return String.valueOf(blockedReason);
+            }
+        }
+
+        private static String allowedReasonToString(int allowedReason) {
+            switch (allowedReason) {
+                case ALLOWED_REASON_NONE:
+                    return "NONE";
+                case ALLOWED_REASON_SYSTEM:
+                    return "SYSTEM";
+                case ALLOWED_REASON_FOREGROUND:
+                    return "FOREGROUND";
+                case ALLOWED_REASON_POWER_SAVE_ALLOWLIST:
+                    return "POWER_SAVE_ALLOWLIST";
+                case ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST:
+                    return "POWER_SAVE_EXCEPT_IDLE_ALLOWLIST";
+                case ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS:
+                    return "RESTRICTED_MODE_PERMISSIONS";
+                case ALLOWED_METERED_REASON_USER_EXEMPTED:
+                    return "METERED_USER_EXEMPTED";
+                case ALLOWED_METERED_REASON_SYSTEM:
+                    return "METERED_SYSTEM";
+                case ALLOWED_METERED_REASON_FOREGROUND:
+                    return "METERED_FOREGROUND";
+                default:
+                    Slog.wtfStack(TAG, "Unknown allowedReason: " + allowedReason);
+                    return String.valueOf(allowedReason);
+            }
+        }
+
+        public static String blockedReasonsToString(int blockedReasons) {
+            if (blockedReasons == BLOCKED_REASON_NONE) {
+                return blockedReasonToString(BLOCKED_REASON_NONE);
+            }
+            final StringBuilder sb = new StringBuilder();
+            for (int reason : BLOCKED_REASONS) {
+                if ((blockedReasons & reason) != 0) {
+                    sb.append(sb.length() == 0 ? "" : "|");
+                    sb.append(blockedReasonToString(reason));
+                    blockedReasons &= ~reason;
+                }
+            }
+            if (blockedReasons != 0) {
+                sb.append(sb.length() == 0 ? "" : "|");
+                sb.append(String.valueOf(blockedReasons));
+                Slog.wtfStack(TAG, "Unknown blockedReasons: " + blockedReasons);
+            }
+            return sb.toString();
+        }
+
+        public static String allowedReasonsToString(int allowedReasons) {
+            if (allowedReasons == ALLOWED_REASON_NONE) {
+                return allowedReasonToString(ALLOWED_REASON_NONE);
+            }
+            final StringBuilder sb = new StringBuilder();
+            for (int reason : ALLOWED_REASONS) {
+                if ((allowedReasons & reason) != 0) {
+                    sb.append(sb.length() == 0 ? "" : "|");
+                    sb.append(allowedReasonToString(reason));
+                    allowedReasons &= ~reason;
+                }
+            }
+            if (allowedReasons != 0) {
+                sb.append(sb.length() == 0 ? "" : "|");
+                sb.append(String.valueOf(allowedReasons));
+                Slog.wtfStack(TAG, "Unknown allowedReasons: " + allowedReasons);
+            }
+            return sb.toString();
+        }
+
+        public void copyFrom(UidBlockedState uidBlockedState) {
+            blockedReasons = uidBlockedState.blockedReasons;
+            allowedReasons = uidBlockedState.allowedReasons;
+            effectiveBlockedReasons = uidBlockedState.effectiveBlockedReasons;
+        }
+
+        public int deriveUidRules() {
+            int uidRule = RULE_NONE;
+            if ((effectiveBlockedReasons & BLOCKED_REASON_RESTRICTED_MODE) != 0) {
+                uidRule |= RULE_REJECT_RESTRICTED_MODE;
+            }
+
+            int powerBlockedReasons = BLOCKED_REASON_APP_STANDBY
+                    | BLOCKED_REASON_DOZE
+                    | BLOCKED_REASON_BATTERY_SAVER;
+            if ((effectiveBlockedReasons & powerBlockedReasons) != 0) {
+                uidRule |= RULE_REJECT_ALL;
+            } else if ((blockedReasons & powerBlockedReasons) != 0) {
+                uidRule |= RULE_ALLOW_ALL;
+            }
+
+            // UidRule doesn't include RestrictBackground (DataSaver) state, so not including in
+            // metered blocked reasons below.
+            int meteredBlockedReasons = BLOCKED_METERED_REASON_ADMIN_DISABLED
+                    | BLOCKED_METERED_REASON_USER_RESTRICTED;
+            if ((effectiveBlockedReasons & meteredBlockedReasons) != 0) {
+                uidRule |= RULE_REJECT_METERED;
+            } else if ((blockedReasons & BLOCKED_METERED_REASON_USER_RESTRICTED) != 0
+                    && (allowedReasons & ALLOWED_METERED_REASON_FOREGROUND) != 0) {
+                uidRule |= RULE_TEMPORARY_ALLOW_METERED;
+            } else if ((blockedReasons & BLOCKED_METERED_REASON_DATA_SAVER) != 0) {
+                if ((allowedReasons & ALLOWED_METERED_REASON_USER_EXEMPTED) != 0) {
+                    uidRule |= RULE_ALLOW_ALL;
+                } else if ((allowedReasons & ALLOWED_METERED_REASON_FOREGROUND) != 0) {
+                    uidRule |= RULE_TEMPORARY_ALLOW_METERED;
+                }
+            }
+            if (LOGV) {
+                Slog.v(TAG, "uidBlockedState=" + this.toString()
+                        + " -> uidRule=" + uidRulesToString(uidRule));
+            }
+            return uidRule;
+        }
     }
 
-    private class NotificationId {
+    private static class NotificationId {
         private final String mTag;
         private final int mId;
 
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java
index f4b72a1..c876d41 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/services/core/java/com/android/server/net/NetworkStatsService.java
@@ -150,6 +150,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.BinderUtils;
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
 
@@ -2097,14 +2098,18 @@
 
         @Override
         public void notifyAlertReached() throws RemoteException {
-            mAlertObserver.limitReached(LIMIT_GLOBAL_ALERT, null /* unused */);
+            // This binder object can only have been obtained by a process that holds
+            // NETWORK_STATS_PROVIDER. Thus, no additional permission check is required.
+            BinderUtils.withCleanCallingIdentity(() ->
+                    mAlertObserver.limitReached(LIMIT_GLOBAL_ALERT, null /* unused */));
         }
 
         @Override
         public void notifyWarningOrLimitReached() {
             Log.d(TAG, mTag + ": notifyWarningOrLimitReached");
-            LocalServices.getService(NetworkPolicyManagerInternal.class)
-                    .onStatsProviderWarningOrLimitReached(mTag);
+            BinderUtils.withCleanCallingIdentity(() ->
+                    LocalServices.getService(NetworkPolicyManagerInternal.class)
+                            .onStatsProviderWarningOrLimitReached(mTag));
         }
 
         @Override
diff --git a/services/core/java/com/android/server/storage/AppFuseBridge.java b/services/core/java/com/android/server/storage/AppFuseBridge.java
index b00540f..2923148 100644
--- a/services/core/java/com/android/server/storage/AppFuseBridge.java
+++ b/services/core/java/com/android/server/storage/AppFuseBridge.java
@@ -24,7 +24,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.FuseUnavailableMountException;
 import com.android.internal.util.Preconditions;
-import com.android.server.NativeDaemonConnectorException;
+import com.android.server.AppFuseMountException;
 import libcore.io.IoUtils;
 import java.util.concurrent.CountDownLatch;
 
@@ -55,7 +55,7 @@
     }
 
     public ParcelFileDescriptor addBridge(MountScope mountScope)
-            throws FuseUnavailableMountException, NativeDaemonConnectorException {
+            throws FuseUnavailableMountException, AppFuseMountException {
         /*
         ** Dead Lock between Java lock (AppFuseBridge.java) and Native lock (FuseBridgeLoop.cc)
         **
@@ -112,7 +112,7 @@
         try {
             int flags = FileUtils.translateModePfdToPosix(mode);
             return scope.openFile(mountId, fileId, flags);
-        } catch (NativeDaemonConnectorException error) {
+        } catch (AppFuseMountException error) {
             throw new FuseUnavailableMountException(mountId);
         }
     }
@@ -160,9 +160,9 @@
             return mMountResult;
         }
 
-        public abstract ParcelFileDescriptor open() throws NativeDaemonConnectorException;
+        public abstract ParcelFileDescriptor open() throws AppFuseMountException;
         public abstract ParcelFileDescriptor openFile(int mountId, int fileId, int flags)
-                throws NativeDaemonConnectorException;
+                throws AppFuseMountException;
     }
 
     private native long native_new();
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index b9ceec1..2f54f30 100755
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -2985,32 +2985,47 @@
         public void addHardwareInput(int deviceId, TvInputInfo inputInfo) {
             ensureHardwarePermission();
             ensureValidInput(inputInfo);
-            synchronized (mLock) {
-                mTvInputHardwareManager.addHardwareInput(deviceId, inputInfo);
-                addHardwareInputLocked(inputInfo);
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mTvInputHardwareManager.addHardwareInput(deviceId, inputInfo);
+                    addHardwareInputLocked(inputInfo);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
             }
         }
 
         public void addHdmiInput(int id, TvInputInfo inputInfo) {
             ensureHardwarePermission();
             ensureValidInput(inputInfo);
-            synchronized (mLock) {
-                mTvInputHardwareManager.addHdmiInput(id, inputInfo);
-                addHardwareInputLocked(inputInfo);
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mTvInputHardwareManager.addHdmiInput(id, inputInfo);
+                    addHardwareInputLocked(inputInfo);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
             }
         }
 
         public void removeHardwareInput(String inputId) {
             ensureHardwarePermission();
-            synchronized (mLock) {
-                ServiceState serviceState = getServiceStateLocked(mComponent, mUserId);
-                boolean removed = serviceState.hardwareInputMap.remove(inputId) != null;
-                if (removed) {
-                    buildTvInputListLocked(mUserId, null);
-                    mTvInputHardwareManager.removeHardwareInput(inputId);
-                } else {
-                    Slog.e(TAG, "failed to remove input " + inputId);
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    ServiceState serviceState = getServiceStateLocked(mComponent, mUserId);
+                    boolean removed = serviceState.hardwareInputMap.remove(inputId) != null;
+                    if (removed) {
+                        buildTvInputListLocked(mUserId, null);
+                        mTvInputHardwareManager.removeHardwareInput(inputId);
+                    } else {
+                        Slog.e(TAG, "failed to remove input " + inputId);
+                    }
                 }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
             }
         }
     }
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 1c46ac8..be13168 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -87,9 +87,10 @@
 import com.android.internal.util.StateMachine;
 import com.android.internal.util.WakeupMessage;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
-import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord;
-import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkTrackerCallback;
 import com.android.server.vcn.Vcn.VcnGatewayStatusCallback;
+import com.android.server.vcn.routeselection.UnderlyingNetworkController;
+import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback;
+import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
 import com.android.server.vcn.util.LogUtils;
 import com.android.server.vcn.util.MtuUtils;
 import com.android.server.vcn.util.OneWayBoolean;
@@ -201,7 +202,7 @@
     private interface EventInfo {}
 
     /**
-     * Sent when there are changes to the underlying network (per the UnderlyingNetworkTracker).
+     * Sent when there are changes to the underlying network (per the UnderlyingNetworkController).
      *
      * <p>May indicate an entirely new underlying network, OR a change in network properties.
      *
@@ -522,11 +523,14 @@
 
     @NonNull private final VcnContext mVcnContext;
     @NonNull private final ParcelUuid mSubscriptionGroup;
-    @NonNull private final UnderlyingNetworkTracker mUnderlyingNetworkTracker;
+    @NonNull private final UnderlyingNetworkController mUnderlyingNetworkController;
     @NonNull private final VcnGatewayConnectionConfig mConnectionConfig;
     @NonNull private final VcnGatewayStatusCallback mGatewayStatusCallback;
     @NonNull private final Dependencies mDeps;
-    @NonNull private final VcnUnderlyingNetworkTrackerCallback mUnderlyingNetworkTrackerCallback;
+
+    @NonNull
+    private final VcnUnderlyingNetworkControllerCallback mUnderlyingNetworkControllerCallback;
+
     private final boolean mIsMobileDataEnabled;
 
     @NonNull private final IpSecManager mIpSecManager;
@@ -674,17 +678,17 @@
 
         mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot");
 
-        mUnderlyingNetworkTrackerCallback = new VcnUnderlyingNetworkTrackerCallback();
+        mUnderlyingNetworkControllerCallback = new VcnUnderlyingNetworkControllerCallback();
 
         mWakeLock =
                 mDeps.newWakeLock(mVcnContext.getContext(), PowerManager.PARTIAL_WAKE_LOCK, TAG);
 
-        mUnderlyingNetworkTracker =
-                mDeps.newUnderlyingNetworkTracker(
+        mUnderlyingNetworkController =
+                mDeps.newUnderlyingNetworkController(
                         mVcnContext,
                         subscriptionGroup,
                         mLastSnapshot,
-                        mUnderlyingNetworkTrackerCallback);
+                        mUnderlyingNetworkControllerCallback);
         mIpSecManager = mVcnContext.getContext().getSystemService(IpSecManager.class);
 
         addState(mDisconnectedState);
@@ -748,7 +752,7 @@
         cancelRetryTimeoutAlarm();
         cancelSafeModeAlarm();
 
-        mUnderlyingNetworkTracker.teardown();
+        mUnderlyingNetworkController.teardown();
 
         mGatewayStatusCallback.onQuit();
     }
@@ -764,12 +768,13 @@
         mVcnContext.ensureRunningOnLooperThread();
 
         mLastSnapshot = snapshot;
-        mUnderlyingNetworkTracker.updateSubscriptionSnapshot(mLastSnapshot);
+        mUnderlyingNetworkController.updateSubscriptionSnapshot(mLastSnapshot);
 
         sendMessageAndAcquireWakeLock(EVENT_SUBSCRIPTIONS_CHANGED, TOKEN_ALL);
     }
 
-    private class VcnUnderlyingNetworkTrackerCallback implements UnderlyingNetworkTrackerCallback {
+    private class VcnUnderlyingNetworkControllerCallback
+            implements UnderlyingNetworkControllerCallback {
         @Override
         public void onSelectedUnderlyingNetworkChanged(
                 @Nullable UnderlyingNetworkRecord underlying) {
@@ -2264,7 +2269,7 @@
                         + (mNetworkAgent == null ? null : mNetworkAgent.getNetwork()));
         pw.println();
 
-        mUnderlyingNetworkTracker.dump(pw);
+        mUnderlyingNetworkController.dump(pw);
         pw.println();
 
         pw.decreaseIndent();
@@ -2276,8 +2281,8 @@
     }
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
-    UnderlyingNetworkTrackerCallback getUnderlyingNetworkTrackerCallback() {
-        return mUnderlyingNetworkTrackerCallback;
+    UnderlyingNetworkControllerCallback getUnderlyingNetworkControllerCallback() {
+        return mUnderlyingNetworkControllerCallback;
     }
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
@@ -2356,17 +2361,14 @@
     /** External dependencies used by VcnGatewayConnection, for injection in tests */
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     public static class Dependencies {
-        /** Builds a new UnderlyingNetworkTracker. */
-        public UnderlyingNetworkTracker newUnderlyingNetworkTracker(
+        /** Builds a new UnderlyingNetworkController. */
+        public UnderlyingNetworkController newUnderlyingNetworkController(
                 VcnContext vcnContext,
                 ParcelUuid subscriptionGroup,
                 TelephonySubscriptionSnapshot snapshot,
-                UnderlyingNetworkTrackerCallback callback) {
-            return new UnderlyingNetworkTracker(
-                    vcnContext,
-                    subscriptionGroup,
-                    snapshot,
-                    callback);
+                UnderlyingNetworkControllerCallback callback) {
+            return new UnderlyingNetworkController(
+                    vcnContext, 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
new file mode 100644
index 0000000..bea8ae9
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
@@ -0,0 +1,196 @@
+/*
+ * 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.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import static com.android.server.VcnManagementService.LOCAL_LOG;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
+import android.net.vcn.VcnManager;
+import android.os.ParcelUuid;
+import android.os.PersistableBundle;
+import android.telephony.SubscriptionManager;
+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 java.util.Set;
+
+/** @hide */
+class NetworkPriorityClassifier {
+    @NonNull private static final String TAG = NetworkPriorityClassifier.class.getSimpleName();
+    /**
+     * Minimum signal strength for a WiFi network to be eligible for switching to
+     *
+     * <p>A network that satisfies this is eligible to become the selected underlying network with
+     * no additional conditions
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT = -70;
+    /**
+     * Minimum signal strength to continue using a WiFi network
+     *
+     * <p>A network that satisfies the conditions may ONLY continue to be used if it is already
+     * selected as the underlying network. A WiFi network satisfying this condition, but NOT the
+     * prospective-network RSSI threshold CANNOT be switched to.
+     */
+    @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(
+            UnderlyingNetworkRecord networkRecord,
+            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) {
+            logWtf("Network blocked for System Server: " + networkRecord.network);
+            return PRIORITY_ANY;
+        }
+
+        if (caps.hasTransport(TRANSPORT_CELLULAR)
+                && isOpportunistic(snapshot, caps.getSubscriptionIds())) {
+            // 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,
+            // or switched away from, the VCN does not attempt to continue using it against the
+            // decision made at the telephony layer. Failure to do so may result in the modem
+            // switching back and forth.
+            //
+            // 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)
+                            .contains(SubscriptionManager.getActiveDataSubscriptionId())
+                    || caps.getSubscriptionIds()
+                            .contains(SubscriptionManager.getActiveDataSubscriptionId())) {
+                return PRIORITY_OPPORTUNISTIC_CELLULAR;
+            }
+        }
+
+        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;
+    }
+
+    static boolean isOpportunistic(
+            @NonNull TelephonySubscriptionSnapshot snapshot, Set<Integer> subIds) {
+        if (snapshot == null) {
+            logWtf("Got null snapshot");
+            return false;
+        }
+        for (int subId : subIds) {
+            if (snapshot.isOpportunistic(subId)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    static int getWifiEntryRssiThreshold(@Nullable PersistableBundle carrierConfig) {
+        if (carrierConfig != null) {
+            return carrierConfig.getInt(
+                    VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
+                    WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT);
+        }
+        return WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT;
+    }
+
+    static int getWifiExitRssiThreshold(@Nullable PersistableBundle carrierConfig) {
+        if (carrierConfig != null) {
+            return carrierConfig.getInt(
+                    VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
+                    WIFI_EXIT_RSSI_THRESHOLD_DEFAULT);
+        }
+        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/UnderlyingNetworkTracker.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
similarity index 60%
rename from services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java
rename to services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 7ddd135..071c7a6 100644
--- a/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -14,13 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.server.vcn;
+package com.android.server.vcn.routeselection;
 
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener;
 
 import static com.android.server.VcnManagementService.LOCAL_LOG;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.getWifiEntryRssiThreshold;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.getWifiExitRssiThreshold;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.isOpportunistic;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -31,27 +32,23 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
-import android.net.vcn.VcnManager;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
 import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
 import android.util.ArrayMap;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
-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.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -61,68 +58,18 @@
 /**
  * Tracks a set of Networks underpinning a VcnGatewayConnection.
  *
- * <p>A single UnderlyingNetworkTracker is built to serve a SINGLE VCN Gateway Connection, and MUST
- * be torn down with the VcnGatewayConnection in order to ensure underlying networks are allowed to
- * be reaped.
+ * <p>A single UnderlyingNetworkController is built to serve a SINGLE VCN Gateway Connection, and
+ * MUST be torn down with the VcnGatewayConnection in order to ensure underlying networks are
+ * allowed to be reaped.
  *
  * @hide
  */
-public class UnderlyingNetworkTracker {
-    @NonNull private static final String TAG = UnderlyingNetworkTracker.class.getSimpleName();
-
-    /**
-     * Minimum signal strength for a WiFi network to be eligible for switching to
-     *
-     * <p>A network that satisfies this is eligible to become the selected underlying network with
-     * no additional conditions
-     */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    static final int WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT = -70;
-
-    /**
-     * Minimum signal strength to continue using a WiFi network
-     *
-     * <p>A network that satisfies the conditions may ONLY continue to be used if it is already
-     * selected as the underlying network. A WiFi network satisfying this condition, but NOT the
-     * prospective-network RSSI threshold CANNOT be switched to.
-     */
-    @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");
-    }
+public class UnderlyingNetworkController {
+    @NonNull private static final String TAG = UnderlyingNetworkController.class.getSimpleName();
 
     @NonNull private final VcnContext mVcnContext;
     @NonNull private final ParcelUuid mSubscriptionGroup;
-    @NonNull private final UnderlyingNetworkTrackerCallback mCb;
+    @NonNull private final UnderlyingNetworkControllerCallback mCb;
     @NonNull private final Dependencies mDeps;
     @NonNull private final Handler mHandler;
     @NonNull private final ConnectivityManager mConnectivityManager;
@@ -142,11 +89,11 @@
     @Nullable private UnderlyingNetworkRecord mCurrentRecord;
     @Nullable private UnderlyingNetworkRecord.Builder mRecordInProgress;
 
-    public UnderlyingNetworkTracker(
+    public UnderlyingNetworkController(
             @NonNull VcnContext vcnContext,
             @NonNull ParcelUuid subscriptionGroup,
             @NonNull TelephonySubscriptionSnapshot snapshot,
-            @NonNull UnderlyingNetworkTrackerCallback cb) {
+            @NonNull UnderlyingNetworkControllerCallback cb) {
         this(
                 vcnContext,
                 subscriptionGroup,
@@ -155,11 +102,11 @@
                 new Dependencies());
     }
 
-    private UnderlyingNetworkTracker(
+    private UnderlyingNetworkController(
             @NonNull VcnContext vcnContext,
             @NonNull ParcelUuid subscriptionGroup,
             @NonNull TelephonySubscriptionSnapshot snapshot,
-            @NonNull UnderlyingNetworkTrackerCallback cb,
+            @NonNull UnderlyingNetworkControllerCallback cb,
             @NonNull Dependencies deps) {
         mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
         mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
@@ -271,8 +218,8 @@
      * subscription group, while the VCN networks are excluded by virtue of not having subIds set on
      * the VCN-exposed networks.
      *
-     * <p>If the VCN that this UnderlyingNetworkTracker belongs to is in test-mode, this will return
-     * a NetworkRequest that only matches Test Networks.
+     * <p>If the VCN that this UnderlyingNetworkController belongs to is in test-mode, this will
+     * return a NetworkRequest that only matches Test Networks.
      */
     private NetworkRequest getRouteSelectionRequest() {
         if (mVcnContext.isInTestMode()) {
@@ -373,9 +320,9 @@
     }
 
     /**
-     * Update this UnderlyingNetworkTracker's TelephonySubscriptionSnapshot.
+     * Update this UnderlyingNetworkController's TelephonySubscriptionSnapshot.
      *
-     * <p>Updating the TelephonySubscriptionSnapshot will cause this UnderlyingNetworkTracker to
+     * <p>Updating the TelephonySubscriptionSnapshot will cause this UnderlyingNetworkController to
      * reevaluate its NetworkBringupCallbacks. This may result in NetworkRequests being registered
      * or unregistered if the subIds mapped to the this Tracker's SubscriptionGroup change.
      */
@@ -410,7 +357,7 @@
 
     private void reevaluateNetworks() {
         if (mIsQuitting || mRouteSelectionCallback == null) {
-            return; // UnderlyingNetworkTracker has quit.
+            return; // UnderlyingNetworkController has quit.
         }
 
         TreeSet<UnderlyingNetworkRecord> sorted =
@@ -424,22 +371,6 @@
         mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord);
     }
 
-    private static boolean isOpportunistic(
-            @NonNull TelephonySubscriptionSnapshot snapshot, Set<Integer> subIds) {
-        if (snapshot == null) {
-            logWtf("Got null snapshot");
-            return false;
-        }
-
-        for (int subId : subIds) {
-            if (snapshot.isOpportunistic(subId)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
     /**
      * NetworkBringupCallback is used to keep background, VCN-managed Networks from being reaped.
      *
@@ -544,230 +475,6 @@
         }
     }
 
-    private static int getWifiEntryRssiThreshold(@Nullable PersistableBundle carrierConfig) {
-        if (carrierConfig != null) {
-            return carrierConfig.getInt(
-                    VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
-                    WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT);
-        }
-
-        return WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT;
-    }
-
-    private static int getWifiExitRssiThreshold(@Nullable PersistableBundle carrierConfig) {
-        if (carrierConfig != null) {
-            return carrierConfig.getInt(
-                    VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
-                    WIFI_EXIT_RSSI_THRESHOLD_DEFAULT);
-        }
-
-        return WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
-    }
-
-    /** A record of a single underlying network, caching relevant fields. */
-    public static class UnderlyingNetworkRecord {
-        @NonNull public final Network network;
-        @NonNull public final NetworkCapabilities networkCapabilities;
-        @NonNull public final LinkProperties linkProperties;
-        public final boolean isBlocked;
-
-        @VisibleForTesting(visibility = Visibility.PRIVATE)
-        UnderlyingNetworkRecord(
-                @NonNull Network network,
-                @NonNull NetworkCapabilities networkCapabilities,
-                @NonNull LinkProperties linkProperties,
-                boolean isBlocked) {
-            this.network = network;
-            this.networkCapabilities = networkCapabilities;
-            this.linkProperties = linkProperties;
-            this.isBlocked = isBlocked;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (!(o instanceof UnderlyingNetworkRecord)) return false;
-            final UnderlyingNetworkRecord that = (UnderlyingNetworkRecord) o;
-
-            return network.equals(that.network)
-                    && networkCapabilities.equals(that.networkCapabilities)
-                    && linkProperties.equals(that.linkProperties)
-                    && isBlocked == that.isBlocked;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(network, networkCapabilities, linkProperties, isBlocked);
-        }
-
-        /**
-         * 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>
-         */
-        private int calculatePriorityClass(
-                ParcelUuid subscriptionGroup,
-                TelephonySubscriptionSnapshot snapshot,
-                UnderlyingNetworkRecord currentlySelected,
-                PersistableBundle carrierConfig) {
-            final NetworkCapabilities caps = networkCapabilities;
-
-            // mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
-
-            if (isBlocked) {
-                logWtf("Network blocked for System Server: " + network);
-                return PRIORITY_ANY;
-            }
-
-            if (caps.hasTransport(TRANSPORT_CELLULAR)
-                    && isOpportunistic(snapshot, caps.getSubscriptionIds())) {
-                // 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,
-                // or switched away from, the VCN does not attempt to continue using it against the
-                // decision made at the telephony layer. Failure to do so may result in the modem
-                // switching back and forth.
-                //
-                // 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)
-                                .contains(SubscriptionManager.getActiveDataSubscriptionId())
-                        || caps.getSubscriptionIds()
-                                .contains(SubscriptionManager.getActiveDataSubscriptionId())) {
-                    return PRIORITY_OPPORTUNISTIC_CELLULAR;
-                }
-            }
-
-            if (caps.hasTransport(TRANSPORT_WIFI)) {
-                if (caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)
-                        && currentlySelected != null
-                        && 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;
-        }
-
-        private static Comparator<UnderlyingNetworkRecord> getComparator(
-                ParcelUuid subscriptionGroup,
-                TelephonySubscriptionSnapshot snapshot,
-                UnderlyingNetworkRecord currentlySelected,
-                PersistableBundle carrierConfig) {
-            return (left, right) -> {
-                return Integer.compare(
-                        left.calculatePriorityClass(
-                                subscriptionGroup, snapshot, currentlySelected, carrierConfig),
-                        right.calculatePriorityClass(
-                                subscriptionGroup, snapshot, currentlySelected, carrierConfig));
-            };
-        }
-
-        /** Dumps the state of this record for logging and debugging purposes. */
-        private void dump(
-                IndentingPrintWriter pw,
-                ParcelUuid subscriptionGroup,
-                TelephonySubscriptionSnapshot snapshot,
-                UnderlyingNetworkRecord currentlySelected,
-                PersistableBundle carrierConfig) {
-            pw.println("UnderlyingNetworkRecord:");
-            pw.increaseIndent();
-
-            final int priorityClass =
-                    calculatePriorityClass(
-                            subscriptionGroup, snapshot, currentlySelected, carrierConfig);
-            pw.println(
-                    "Priority class: " + PRIORITY_TO_STRING_MAP.get(priorityClass) + " ("
-                            + priorityClass + ")");
-            pw.println("mNetwork: " + network);
-            pw.println("mNetworkCapabilities: " + networkCapabilities);
-            pw.println("mLinkProperties: " + linkProperties);
-
-            pw.decreaseIndent();
-        }
-
-        /** Builder to incrementally construct an UnderlyingNetworkRecord. */
-        private static class Builder {
-            @NonNull private final Network mNetwork;
-
-            @Nullable private NetworkCapabilities mNetworkCapabilities;
-            @Nullable private LinkProperties mLinkProperties;
-            boolean mIsBlocked;
-            boolean mWasIsBlockedSet;
-
-            @Nullable private UnderlyingNetworkRecord mCached;
-
-            private Builder(@NonNull Network network) {
-                mNetwork = network;
-            }
-
-            @NonNull
-            private Network getNetwork() {
-                return mNetwork;
-            }
-
-            private void setNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
-                mNetworkCapabilities = networkCapabilities;
-                mCached = null;
-            }
-
-            @Nullable
-            private NetworkCapabilities getNetworkCapabilities() {
-                return mNetworkCapabilities;
-            }
-
-            private void setLinkProperties(@NonNull LinkProperties linkProperties) {
-                mLinkProperties = linkProperties;
-                mCached = null;
-            }
-
-            private void setIsBlocked(boolean isBlocked) {
-                mIsBlocked = isBlocked;
-                mWasIsBlockedSet = true;
-                mCached = null;
-            }
-
-            private boolean isValid() {
-                return mNetworkCapabilities != null && mLinkProperties != null && mWasIsBlockedSet;
-            }
-
-            private UnderlyingNetworkRecord build() {
-                if (!isValid()) {
-                    throw new IllegalArgumentException(
-                            "Called build before UnderlyingNetworkRecord was valid");
-                }
-
-                if (mCached == null) {
-                    mCached =
-                            new UnderlyingNetworkRecord(
-                                    mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
-                }
-
-                return mCached;
-            }
-        }
-    }
-
     private static void logWtf(String msg) {
         Slog.wtf(TAG, msg);
         LOCAL_LOG.log(TAG + " WTF: " + msg);
@@ -780,7 +487,7 @@
 
     /** Dumps the state of this record for logging and debugging purposes. */
     public void dump(IndentingPrintWriter pw) {
-        pw.println("UnderlyingNetworkTracker:");
+        pw.println("UnderlyingNetworkController:");
         pw.increaseIndent();
 
         pw.println("Carrier WiFi Entry Threshold: " + getWifiEntryRssiThreshold(mCarrierConfig));
@@ -811,7 +518,7 @@
     }
 
     /** Callbacks for being notified of the changes in, or to the selected underlying network. */
-    public interface UnderlyingNetworkTrackerCallback {
+    public interface UnderlyingNetworkControllerCallback {
         /**
          * Fired when a new underlying network is selected, or properties have changed.
          *
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
new file mode 100644
index 0000000..65c69de
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
@@ -0,0 +1,175 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.os.ParcelUuid;
+import android.os.PersistableBundle;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * A record of a single underlying network, caching relevant fields.
+ *
+ * @hide
+ */
+public class UnderlyingNetworkRecord {
+    @NonNull public final Network network;
+    @NonNull public final NetworkCapabilities networkCapabilities;
+    @NonNull public final LinkProperties linkProperties;
+    public final boolean isBlocked;
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public UnderlyingNetworkRecord(
+            @NonNull Network network,
+            @NonNull NetworkCapabilities networkCapabilities,
+            @NonNull LinkProperties linkProperties,
+            boolean isBlocked) {
+        this.network = network;
+        this.networkCapabilities = networkCapabilities;
+        this.linkProperties = linkProperties;
+        this.isBlocked = isBlocked;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof UnderlyingNetworkRecord)) return false;
+        final UnderlyingNetworkRecord that = (UnderlyingNetworkRecord) o;
+
+        return network.equals(that.network)
+                && networkCapabilities.equals(that.networkCapabilities)
+                && linkProperties.equals(that.linkProperties)
+                && isBlocked == that.isBlocked;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(network, networkCapabilities, linkProperties, isBlocked);
+    }
+
+    static Comparator<UnderlyingNetworkRecord> getComparator(
+            ParcelUuid subscriptionGroup,
+            TelephonySubscriptionSnapshot snapshot,
+            UnderlyingNetworkRecord currentlySelected,
+            PersistableBundle carrierConfig) {
+        return (left, right) -> {
+            return Integer.compare(
+                    NetworkPriorityClassifier.calculatePriorityClass(
+                            left, subscriptionGroup, snapshot, currentlySelected, carrierConfig),
+                    NetworkPriorityClassifier.calculatePriorityClass(
+                            right, subscriptionGroup, snapshot, currentlySelected, carrierConfig));
+        };
+    }
+
+    /** Dumps the state of this record for logging and debugging purposes. */
+    void dump(
+            IndentingPrintWriter pw,
+            ParcelUuid subscriptionGroup,
+            TelephonySubscriptionSnapshot snapshot,
+            UnderlyingNetworkRecord currentlySelected,
+            PersistableBundle carrierConfig) {
+        pw.println("UnderlyingNetworkRecord:");
+        pw.increaseIndent();
+
+        final int priorityClass =
+                NetworkPriorityClassifier.calculatePriorityClass(
+                        this, subscriptionGroup, snapshot, currentlySelected, carrierConfig);
+        pw.println(
+                "Priority class: "
+                        + NetworkPriorityClassifier.priorityClassToString(priorityClass)
+                        + " ("
+                        + priorityClass
+                        + ")");
+        pw.println("mNetwork: " + network);
+        pw.println("mNetworkCapabilities: " + networkCapabilities);
+        pw.println("mLinkProperties: " + linkProperties);
+
+        pw.decreaseIndent();
+    }
+
+    /** Builder to incrementally construct an UnderlyingNetworkRecord. */
+    static class Builder {
+        @NonNull private final Network mNetwork;
+
+        @Nullable private NetworkCapabilities mNetworkCapabilities;
+        @Nullable private LinkProperties mLinkProperties;
+        boolean mIsBlocked;
+        boolean mWasIsBlockedSet;
+
+        @Nullable private UnderlyingNetworkRecord mCached;
+
+        Builder(@NonNull Network network) {
+            mNetwork = network;
+        }
+
+        @NonNull
+        Network getNetwork() {
+            return mNetwork;
+        }
+
+        void setNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+            mNetworkCapabilities = networkCapabilities;
+            mCached = null;
+        }
+
+        @Nullable
+        NetworkCapabilities getNetworkCapabilities() {
+            return mNetworkCapabilities;
+        }
+
+        void setLinkProperties(@NonNull LinkProperties linkProperties) {
+            mLinkProperties = linkProperties;
+            mCached = null;
+        }
+
+        void setIsBlocked(boolean isBlocked) {
+            mIsBlocked = isBlocked;
+            mWasIsBlockedSet = true;
+            mCached = null;
+        }
+
+        boolean isValid() {
+            return mNetworkCapabilities != null && mLinkProperties != null && mWasIsBlockedSet;
+        }
+
+        UnderlyingNetworkRecord build() {
+            if (!isValid()) {
+                throw new IllegalArgumentException(
+                        "Called build before UnderlyingNetworkRecord was valid");
+            }
+
+            if (mCached == null) {
+                mCached =
+                        new UnderlyingNetworkRecord(
+                                mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
+            }
+
+            return mCached;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/BluetoothAirplaneModeListenerTest.java b/services/tests/servicestests/src/com/android/server/BluetoothAirplaneModeListenerTest.java
index 3ace3f4..a1d4c20 100644
--- a/services/tests/servicestests/src/com/android/server/BluetoothAirplaneModeListenerTest.java
+++ b/services/tests/servicestests/src/com/android/server/BluetoothAirplaneModeListenerTest.java
@@ -66,7 +66,7 @@
         when(mHelper.isBluetoothOn()).thenReturn(true);
         Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
 
-        when(mHelper.isA2dpOrHearingAidConnected()).thenReturn(true);
+        when(mHelper.isMediaProfileConnected()).thenReturn(true);
         Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
 
         when(mHelper.isAirplaneModeOn()).thenReturn(true);
@@ -83,7 +83,7 @@
     public void testHandleAirplaneModeChange_NotInvokeAirplaneModeChanged_NotPopToast() {
         mBluetoothAirplaneModeListener.mToastCount = BluetoothAirplaneModeListener.MAX_TOAST_COUNT;
         when(mHelper.isBluetoothOn()).thenReturn(true);
-        when(mHelper.isA2dpOrHearingAidConnected()).thenReturn(true);
+        when(mHelper.isMediaProfileConnected()).thenReturn(true);
         when(mHelper.isAirplaneModeOn()).thenReturn(true);
         mBluetoothAirplaneModeListener.handleAirplaneModeChange();
 
@@ -97,7 +97,7 @@
     public void testHandleAirplaneModeChange_NotInvokeAirplaneModeChanged_PopToast() {
         mBluetoothAirplaneModeListener.mToastCount = 0;
         when(mHelper.isBluetoothOn()).thenReturn(true);
-        when(mHelper.isA2dpOrHearingAidConnected()).thenReturn(true);
+        when(mHelper.isMediaProfileConnected()).thenReturn(true);
         when(mHelper.isAirplaneModeOn()).thenReturn(true);
         mBluetoothAirplaneModeListener.handleAirplaneModeChange();
 
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index 937f9dc..15de226 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -119,7 +119,7 @@
     @Test
     public void testNullNetworkDoesNotTriggerDisconnect() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(null);
         mTestLooper.dispatchAll();
 
@@ -131,7 +131,7 @@
     @Test
     public void testNewNetworkTriggersMigration() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_2);
         mTestLooper.dispatchAll();
 
@@ -143,7 +143,7 @@
     @Test
     public void testSameNetworkDoesNotTriggerMigration() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_1);
         mTestLooper.dispatchAll();
 
@@ -203,7 +203,7 @@
         triggerChildOpened();
 
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_2);
         getChildSessionCallback()
                 .onIpSecTransformsMigrated(makeDummyIpSecTransform(), makeDummyIpSecTransform());
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
index d1f3a21..3c70759 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
@@ -64,7 +64,7 @@
     @Test
     public void testNullNetworkTriggersDisconnect() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(null);
         mTestLooper.dispatchAll();
 
@@ -76,7 +76,7 @@
     @Test
     public void testNewNetworkTriggersReconnect() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_2);
         mTestLooper.dispatchAll();
 
@@ -89,7 +89,7 @@
     @Test
     public void testSameNetworkDoesNotTriggerReconnect() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_1);
         mTestLooper.dispatchAll();
 
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
index 2056eea..f3eb82f 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
@@ -78,7 +78,7 @@
     @Test
     public void testNetworkChangesTriggerStateTransitions() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_1);
         mTestLooper.dispatchAll();
 
@@ -89,7 +89,7 @@
     @Test
     public void testNullNetworkDoesNotTriggerStateTransition() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(null);
         mTestLooper.dispatchAll();
 
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
index 1c85979..6568cdd 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
@@ -58,7 +58,7 @@
     @Test
     public void testNewNetworkTriggerRetry() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_2);
         mTestLooper.dispatchAll();
 
@@ -72,7 +72,7 @@
     @Test
     public void testSameNetworkDoesNotTriggerRetry() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_1);
         mTestLooper.dispatchAll();
 
@@ -86,7 +86,7 @@
     @Test
     public void testNullNetworkTriggersDisconnect() throws Exception {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(null);
         mTestLooper.dispatchAll();
 
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index 2b0037e..b9dfda3 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -59,7 +59,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
-import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord;
+import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -238,14 +238,14 @@
     }
 
     @Test
-    public void testSubscriptionSnapshotUpdateNotifiesUnderlyingNetworkTracker() {
+    public void testSubscriptionSnapshotUpdateNotifiesUnderlyingNetworkController() {
         verifyWakeLockSetUp();
 
         final TelephonySubscriptionSnapshot updatedSnapshot =
                 mock(TelephonySubscriptionSnapshot.class);
         mGatewayConnection.updateSubscriptionSnapshot(updatedSnapshot);
 
-        verify(mUnderlyingNetworkTracker).updateSubscriptionSnapshot(eq(updatedSnapshot));
+        verify(mUnderlyingNetworkController).updateSubscriptionSnapshot(eq(updatedSnapshot));
         verifyWakeLockAcquired();
 
         mTestLooper.dispatchAll();
@@ -256,13 +256,13 @@
     @Test
     public void testNonNullUnderlyingNetworkRecordUpdateCancelsAlarm() {
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(null);
 
         verifyDisconnectRequestAlarmAndGetCallback(false /* expectCanceled */);
 
         mGatewayConnection
-                .getUnderlyingNetworkTrackerCallback()
+                .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_1);
 
         verify(mDisconnectRequestAlarm).cancel();
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 64d0bca..8a0af2d 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -16,7 +16,6 @@
 
 package com.android.server.vcn;
 
-import static com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord;
 import static com.android.server.vcn.VcnGatewayConnection.VcnIkeSession;
 import static com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent;
 import static com.android.server.vcn.VcnTestUtils.setupIpSecManager;
@@ -62,6 +61,8 @@
 import com.android.server.vcn.Vcn.VcnGatewayStatusCallback;
 import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback;
 import com.android.server.vcn.VcnGatewayConnection.VcnWakeLock;
+import com.android.server.vcn.routeselection.UnderlyingNetworkController;
+import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
 
 import org.junit.Before;
 import org.mockito.ArgumentCaptor;
@@ -137,7 +138,7 @@
     @NonNull protected final VcnGatewayConnectionConfig mConfig;
     @NonNull protected final VcnGatewayStatusCallback mGatewayStatusCallback;
     @NonNull protected final VcnGatewayConnection.Dependencies mDeps;
-    @NonNull protected final UnderlyingNetworkTracker mUnderlyingNetworkTracker;
+    @NonNull protected final UnderlyingNetworkController mUnderlyingNetworkController;
     @NonNull protected final VcnWakeLock mWakeLock;
     @NonNull protected final WakeupMessage mTeardownTimeoutAlarm;
     @NonNull protected final WakeupMessage mDisconnectRequestAlarm;
@@ -158,7 +159,7 @@
         mConfig = VcnGatewayConnectionConfigTest.buildTestConfig();
         mGatewayStatusCallback = mock(VcnGatewayStatusCallback.class);
         mDeps = mock(VcnGatewayConnection.Dependencies.class);
-        mUnderlyingNetworkTracker = mock(UnderlyingNetworkTracker.class);
+        mUnderlyingNetworkController = mock(UnderlyingNetworkController.class);
         mWakeLock = mock(VcnWakeLock.class);
         mTeardownTimeoutAlarm = mock(WakeupMessage.class);
         mDisconnectRequestAlarm = mock(WakeupMessage.class);
@@ -176,9 +177,9 @@
         doReturn(mTestLooper.getLooper()).when(mVcnContext).getLooper();
         doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider();
 
-        doReturn(mUnderlyingNetworkTracker)
+        doReturn(mUnderlyingNetworkController)
                 .when(mDeps)
-                .newUnderlyingNetworkTracker(any(), any(), any(), any());
+                .newUnderlyingNetworkController(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/UnderlyingNetworkTrackerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
similarity index 86%
rename from tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java
rename to tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index 5af69b5..c954cb8 100644
--- a/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.server.vcn;
+package com.android.server.vcn.routeselection;
 
 import static com.android.server.vcn.VcnTestUtils.setupSystemService;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
@@ -48,10 +50,11 @@
 import android.util.ArraySet;
 
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
-import com.android.server.vcn.UnderlyingNetworkTracker.NetworkBringupCallback;
-import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkListener;
-import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord;
-import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkTrackerCallback;
+import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.VcnNetworkProvider;
+import com.android.server.vcn.routeselection.UnderlyingNetworkController.NetworkBringupCallback;
+import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback;
+import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -64,7 +67,7 @@
 import java.util.Set;
 import java.util.UUID;
 
-public class UnderlyingNetworkTrackerTest {
+public class UnderlyingNetworkControllerTest {
     private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
     private static final int INITIAL_SUB_ID_1 = 1;
     private static final int INITIAL_SUB_ID_2 = 2;
@@ -102,14 +105,14 @@
     @Mock private TelephonyManager mTelephonyManager;
     @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
-    @Mock private UnderlyingNetworkTrackerCallback mNetworkTrackerCb;
+    @Mock private UnderlyingNetworkControllerCallback mNetworkControllerCb;
     @Mock private Network mNetwork;
 
     @Captor private ArgumentCaptor<UnderlyingNetworkListener> mUnderlyingNetworkListenerCaptor;
 
     private TestLooper mTestLooper;
     private VcnContext mVcnContext;
-    private UnderlyingNetworkTracker mUnderlyingNetworkTracker;
+    private UnderlyingNetworkController mUnderlyingNetworkController;
 
     @Before
     public void setUp() {
@@ -140,12 +143,9 @@
 
         when(mSubscriptionSnapshot.getAllSubIdsInGroup(eq(SUB_GROUP))).thenReturn(INITIAL_SUB_IDS);
 
-        mUnderlyingNetworkTracker =
-                new UnderlyingNetworkTracker(
-                        mVcnContext,
-                        SUB_GROUP,
-                        mSubscriptionSnapshot,
-                        mNetworkTrackerCb);
+        mUnderlyingNetworkController =
+                new UnderlyingNetworkController(
+                        mVcnContext, SUB_GROUP, mSubscriptionSnapshot, mNetworkControllerCb);
     }
 
     private void resetVcnContext() {
@@ -181,11 +181,8 @@
                         mVcnNetworkProvider,
                         true /* isInTestMode */);
 
-        new UnderlyingNetworkTracker(
-                vcnContext,
-                SUB_GROUP,
-                mSubscriptionSnapshot,
-                mNetworkTrackerCb);
+        new UnderlyingNetworkController(
+                vcnContext, SUB_GROUP, mSubscriptionSnapshot, mNetworkControllerCb);
 
         verify(cm)
                 .registerNetworkCallback(
@@ -233,7 +230,7 @@
                 mock(TelephonySubscriptionSnapshot.class);
         when(subscriptionUpdate.getAllSubIdsInGroup(eq(SUB_GROUP))).thenReturn(UPDATED_SUB_IDS);
 
-        mUnderlyingNetworkTracker.updateSubscriptionSnapshot(subscriptionUpdate);
+        mUnderlyingNetworkController.updateSubscriptionSnapshot(subscriptionUpdate);
 
         // verify that initially-filed bringup requests are unregistered (cell + wifi)
         verify(mConnectivityManager, times(INITIAL_SUB_IDS.size() + 3))
@@ -255,7 +252,7 @@
         return getExpectedRequestBase()
                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                 .setSubscriptionIds(netCapsSubIds)
-                .setSignalStrength(UnderlyingNetworkTracker.WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT)
+                .setSignalStrength(WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT)
                 .build();
     }
 
@@ -264,7 +261,7 @@
         return getExpectedRequestBase()
                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                 .setSubscriptionIds(netCapsSubIds)
-                .setSignalStrength(UnderlyingNetworkTracker.WIFI_EXIT_RSSI_THRESHOLD_DEFAULT)
+                .setSignalStrength(WIFI_EXIT_RSSI_THRESHOLD_DEFAULT)
                 .build();
     }
 
@@ -304,7 +301,7 @@
 
     @Test
     public void testTeardown() {
-        mUnderlyingNetworkTracker.teardown();
+        mUnderlyingNetworkController.teardown();
 
         // Expect 5 NetworkBringupCallbacks to be unregistered: 1 for WiFi, 2 for Cellular (1x for
         // each subId), and 1 for each of the Wifi signal strength thresholds
@@ -368,7 +365,7 @@
                         networkCapabilities,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
         return cb;
     }
 
@@ -384,7 +381,7 @@
                         UPDATED_NETWORK_CAPABILITIES,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
 
     @Test
@@ -399,7 +396,7 @@
                         INITIAL_NETWORK_CAPABILITIES,
                         UPDATED_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
 
     @Test
@@ -414,11 +411,13 @@
                         SUSPENDED_NETWORK_CAPABILITIES,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkTrackerCb, times(1)).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        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);
-        verify(mNetworkTrackerCb, times(1)).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verify(mNetworkControllerCb, times(1))
+                .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
 
     @Test
@@ -434,11 +433,13 @@
                         INITIAL_NETWORK_CAPABILITIES,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkTrackerCb, times(1)).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        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);
-        verify(mNetworkTrackerCb, times(1)).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verify(mNetworkControllerCb, times(1))
+                .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
 
     @Test
@@ -453,7 +454,7 @@
                         INITIAL_NETWORK_CAPABILITIES,
                         INITIAL_LINK_PROPERTIES,
                         true /* isBlocked */);
-        verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
 
     @Test
@@ -462,7 +463,7 @@
 
         cb.onLost(mNetwork);
 
-        verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(null);
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(null);
     }
 
     @Test
@@ -471,20 +472,20 @@
 
         cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
 
-        // Verify no more calls to the UnderlyingNetworkTrackerCallback when the
+        // Verify no more calls to the UnderlyingNetworkControllerCallback when the
         // UnderlyingNetworkRecord does not actually change
-        verifyNoMoreInteractions(mNetworkTrackerCb);
+        verifyNoMoreInteractions(mNetworkControllerCb);
     }
 
     @Test
     public void testRecordTrackerCallbackNotifiedAfterTeardown() {
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
-        mUnderlyingNetworkTracker.teardown();
+        mUnderlyingNetworkController.teardown();
 
         cb.onCapabilitiesChanged(mNetwork, UPDATED_NETWORK_CAPABILITIES);
 
         // Verify that the only call was during onAvailable()
-        verify(mNetworkTrackerCb, times(1)).onSelectedUnderlyingNetworkChanged(any());
+        verify(mNetworkControllerCb, times(1)).onSelectedUnderlyingNetworkChanged(any());
     }
 
     // TODO (b/187991063): Add tests for network prioritization