Merge "Replace "mts" with "mts-tethering"."
diff --git a/TEST_MAPPING b/TEST_MAPPING
index be0e040..90312a4 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -12,6 +12,9 @@
       "options": [
         {
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
     },
@@ -33,6 +36,9 @@
       "options": [
         {
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
     },
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 495c9d7..e4ce615 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -33,6 +33,7 @@
         "NetworkStackApiStableShims",
         "androidx.annotation_annotation",
         "modules-utils-build",
+        "modules-utils-statemachine",
         "networkstack-client",
         "android.hardware.tetheroffload.config-V1.0-java",
         "android.hardware.tetheroffload.control-V1.0-java",
@@ -41,7 +42,6 @@
         "net-utils-device-common",
         "net-utils-device-common-netlink",
         "netd-client",
-        "NetworkStackApiCurrentShims",
     ],
     libs: [
         "framework-connectivity",
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 69bb71f..9e6e34e 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -37,6 +37,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -265,7 +266,7 @@
     public TetheringManager(@NonNull final Context context,
             @NonNull Supplier<IBinder> connectorSupplier) {
         mContext = context;
-        mCallback = new TetheringCallbackInternal();
+        mCallback = new TetheringCallbackInternal(this);
         mConnectorSupplier = connectorSupplier;
 
         final String pkgName = mContext.getOpPackageName();
@@ -289,6 +290,23 @@
         getConnector(c -> c.registerTetheringEventCallback(mCallback, pkgName));
     }
 
+    /** @hide */
+    @Override
+    protected void finalize() throws Throwable {
+        final String pkgName = mContext.getOpPackageName();
+        Log.i(TAG, "unregisterTetheringEventCallback:" + pkgName);
+        // 1. It's generally not recommended to perform long operations in finalize, but while
+        // unregisterTetheringEventCallback does an IPC, it's a oneway IPC so should not block.
+        // 2. If the connector is not yet connected, TetheringManager is impossible to finalize
+        // because the connector polling thread strong reference the TetheringManager object. So
+        // it's guaranteed that registerTetheringEventCallback was already called before calling
+        // unregisterTetheringEventCallback in finalize.
+        if (mConnector == null) Log.wtf(TAG, "null connector in finalize!");
+        getConnector(c -> c.unregisterTetheringEventCallback(mCallback, pkgName));
+
+        super.finalize();
+    }
+
     private void startPollingForConnector() {
         new Thread(() -> {
             while (true) {
@@ -415,7 +433,7 @@
         }
     }
 
-    private void throwIfPermissionFailure(final int errorCode) {
+    private static void throwIfPermissionFailure(final int errorCode) {
         switch (errorCode) {
             case TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:
                 throw new SecurityException("No android.permission.TETHER_PRIVILEGED"
@@ -426,21 +444,40 @@
         }
     }
 
-    private class TetheringCallbackInternal extends ITetheringEventCallback.Stub {
+    private static class TetheringCallbackInternal extends ITetheringEventCallback.Stub {
         private volatile int mError = TETHER_ERROR_NO_ERROR;
         private final ConditionVariable mWaitForCallback = new ConditionVariable();
+        // This object is never garbage collected because the Tethering code running in
+        // the system server always maintains a reference to it for as long as
+        // mCallback is registered.
+        //
+        // Don't keep a strong reference to TetheringManager because otherwise
+        // TetheringManager cannot be garbage collected, and because TetheringManager
+        // stores the Context that it was created from, this will prevent the calling
+        // Activity from being garbage collected as well.
+        private final WeakReference<TetheringManager> mTetheringMgrRef;
+
+        TetheringCallbackInternal(final TetheringManager tm) {
+            mTetheringMgrRef = new WeakReference<>(tm);
+        }
 
         @Override
         public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
-            mTetheringConfiguration = parcel.config;
-            mTetherStatesParcel = parcel.states;
-            mWaitForCallback.open();
+            TetheringManager tetheringMgr = mTetheringMgrRef.get();
+            if (tetheringMgr != null) {
+                tetheringMgr.mTetheringConfiguration = parcel.config;
+                tetheringMgr.mTetherStatesParcel = parcel.states;
+                mWaitForCallback.open();
+            }
         }
 
         @Override
         public void onCallbackStopped(int errorCode) {
-            mError = errorCode;
-            mWaitForCallback.open();
+            TetheringManager tetheringMgr = mTetheringMgrRef.get();
+            if (tetheringMgr != null) {
+                mError = errorCode;
+                mWaitForCallback.open();
+            }
         }
 
         @Override
@@ -448,12 +485,14 @@
 
         @Override
         public void onConfigurationChanged(TetheringConfigurationParcel config) {
-            mTetheringConfiguration = config;
+            TetheringManager tetheringMgr = mTetheringMgrRef.get();
+            if (tetheringMgr != null) tetheringMgr.mTetheringConfiguration = config;
         }
 
         @Override
         public void onTetherStatesChanged(TetherStatesParcel states) {
-            mTetherStatesParcel = states;
+            TetheringManager tetheringMgr = mTetheringMgrRef.get();
+            if (tetheringMgr != null) tetheringMgr.mTetherStatesParcel = states;
         }
 
         @Override
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index cf49683..bee928d 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -46,8 +46,6 @@
 import android.net.dhcp.IDhcpServer;
 import android.net.ip.IpNeighborMonitor.NeighborEvent;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.shared.NetdUtils;
-import android.net.shared.RouteUtils;
 import android.net.util.InterfaceParams;
 import android.net.util.InterfaceSet;
 import android.net.util.PrefixUtils;
@@ -67,6 +65,7 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.NetdUtils;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
@@ -767,7 +766,7 @@
     }
 
     private void removeRoutesFromLocalNetwork(@NonNull final List<RouteInfo> toBeRemoved) {
-        final int removalFailures = RouteUtils.removeRoutesFromLocalNetwork(
+        final int removalFailures = NetdUtils.removeRoutesFromLocalNetwork(
                 mNetd, toBeRemoved);
         if (removalFailures > 0) {
             mLog.e(String.format("Failed to remove %d IPv6 routes from local table.",
@@ -785,7 +784,7 @@
             try {
                 // Add routes from local network. Note that adding routes that
                 // already exist does not cause an error (EEXIST is silently ignored).
-                RouteUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
+                NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
             } catch (IllegalStateException e) {
                 mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
                 return;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 1559f3b..c942899 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -1140,9 +1140,7 @@
             final WifiP2pGroup group =
                     (WifiP2pGroup) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
 
-            if (VDBG) {
-                Log.d(TAG, "WifiP2pAction: P2pInfo: " + p2pInfo + " Group: " + group);
-            }
+            mLog.i("WifiP2pAction: P2pInfo: " + p2pInfo + " Group: " + group);
 
             // if no group is formed, bring it down if needed.
             if (p2pInfo == null || !p2pInfo.groupFormed) {
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index 1d94214..34f3e0e 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -44,7 +44,6 @@
 import android.net.MacAddress;
 import android.net.RouteInfo;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.shared.RouteUtils;
 import android.net.util.InterfaceParams;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -56,6 +55,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
@@ -335,7 +335,7 @@
         final String iface = mTetheredParams.name;
         final RouteInfo linkLocalRoute =
                 new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST);
-        RouteUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute));
+        NetdUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute));
 
         final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788");
         mTetheredPacketReader.sendResponse(rs);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
index 4865e03..3c07580 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
@@ -22,7 +22,6 @@
 
 import android.content.Context;
 import android.content.Intent;
-import android.net.ITetheringConnector;
 import android.os.Binder;
 import android.os.IBinder;
 import android.util.ArrayMap;
@@ -72,8 +71,8 @@
             mBase = base;
         }
 
-        public ITetheringConnector getTetheringConnector() {
-            return ITetheringConnector.Stub.asInterface(mBase);
+        public IBinder getIBinder() {
+            return mBase;
         }
 
         public MockTetheringService getService() {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index 1b52f6e..f664d5d 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -26,6 +26,9 @@
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
@@ -40,10 +43,13 @@
 import android.net.IIntResultListener;
 import android.net.ITetheringConnector;
 import android.net.ITetheringEventCallback;
+import android.net.TetheringManager;
 import android.net.TetheringRequestParcel;
 import android.net.ip.IpServer;
 import android.os.Bundle;
+import android.os.ConditionVariable;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.ResultReceiver;
 
 import androidx.test.InstrumentationRegistry;
@@ -62,6 +68,10 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.function.Supplier;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public final class TetheringServiceTest {
@@ -113,7 +123,7 @@
                 InstrumentationRegistry.getTargetContext(),
                 MockTetheringService.class);
         mMockConnector = (MockTetheringConnector) mServiceTestRule.bindService(mMockServiceIntent);
-        mTetheringConnector = mMockConnector.getTetheringConnector();
+        mTetheringConnector = ITetheringConnector.Stub.asInterface(mMockConnector.getIBinder());
         final MockTetheringService service = mMockConnector.getService();
         mTethering = service.getTethering();
     }
@@ -493,4 +503,81 @@
             verifyNoMoreInteractionsForTethering();
         });
     }
+
+    private class ConnectorSupplier<T> implements Supplier<T> {
+        private T mResult = null;
+
+        public void set(T result) {
+            mResult = result;
+        }
+
+        @Override
+        public T get() {
+            return mResult;
+        }
+    }
+
+    private void forceGc() {
+        System.gc();
+        System.runFinalization();
+        System.gc();
+    }
+
+    @Test
+    public void testTetheringManagerLeak() throws Exception {
+        runAsAccessNetworkState((none) -> {
+            final ArrayList<ITetheringEventCallback> callbacks = new ArrayList<>();
+            final ConditionVariable registeredCv = new ConditionVariable(false);
+            doAnswer((invocation) -> {
+                final Object[] args = invocation.getArguments();
+                callbacks.add((ITetheringEventCallback) args[0]);
+                registeredCv.open();
+                return null;
+            }).when(mTethering).registerTetheringEventCallback(any());
+
+            doAnswer((invocation) -> {
+                final Object[] args = invocation.getArguments();
+                callbacks.remove((ITetheringEventCallback) args[0]);
+                return null;
+            }).when(mTethering).unregisterTetheringEventCallback(any());
+
+            final ConnectorSupplier<IBinder> supplier = new ConnectorSupplier<>();
+
+            TetheringManager tm = new TetheringManager(mMockConnector.getService(), supplier);
+            assertNotNull(tm);
+            assertEquals("Internal callback should not be registered", 0, callbacks.size());
+
+            final WeakReference<TetheringManager> weakTm = new WeakReference(tm);
+            assertNotNull(weakTm.get());
+
+            // TetheringManager couldn't be GCed because pollingConnector thread implicitly
+            // reference TetheringManager object.
+            tm = null;
+            forceGc();
+            assertNotNull(weakTm.get());
+
+            // After getting connector, pollingConnector thread stops and internal callback is
+            // registered.
+            supplier.set(mMockConnector.getIBinder());
+            final long timeout = 500L;
+            if (!registeredCv.block(timeout)) {
+                fail("TetheringManager poll connector fail after " + timeout + " ms");
+            }
+            assertEquals("Internal callback is not registered", 1, callbacks.size());
+            assertNotNull(weakTm.get());
+
+            final int attempts = 100;
+            final long waitIntervalMs = 50;
+            for (int i = 0; i < attempts; i++) {
+                forceGc();
+                if (weakTm.get() == null) break;
+
+                Thread.sleep(waitIntervalMs);
+            }
+            assertNull("TetheringManager weak reference still not null after " + attempts
+                    + " attempts", weakTm.get());
+
+            assertEquals("Internal callback is not unregistered", 0, callbacks.size());
+        });
+    }
 }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 8309869..c21bcfa 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -46,7 +46,6 @@
 import android.net.TetheringManager.StartTetheringCallback;
 import android.net.TetheringManager.TetheringEventCallback;
 import android.net.TetheringManager.TetheringRequest;
-import android.net.wifi.WifiNetworkSuggestion;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Build.VERSION_CODES;
@@ -3479,7 +3478,8 @@
          * {@link NetworkCapabilities#getTransportInfo()}) like {@link android.net.wifi.WifiInfo}
          * contain location sensitive information.
          * <li> OwnerUid (retrieved via {@link NetworkCapabilities#getOwnerUid()} is location
-         * sensitive for wifi suggestor apps (i.e using {@link WifiNetworkSuggestion}).</li>
+         * sensitive for wifi suggestor apps (i.e using
+         * {@link android.net.wifi.WifiNetworkSuggestion WifiNetworkSuggestion}).</li>
          * </p>
          * <p>
          * Note:
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index e9bcd95..75f0129 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -27,7 +27,6 @@
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.net.ConnectivityManager.NetworkCallback;
-import android.net.wifi.WifiNetworkSuggestion;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -1192,14 +1191,14 @@
      *
      * <p>
      * This field will only be populated for VPN and wifi network suggestor apps (i.e using
-     * {@link WifiNetworkSuggestion}), and only for the network they own.
-     * In the case of wifi network suggestors apps, this field is also location sensitive, so the
-     * app needs to hold {@link android.Manifest.permission#ACCESS_FINE_LOCATION} permission. If the
-     * app targets SDK version greater than or equal to {@link Build.VERSION_CODES#S}, then they
-     * also need to use {@link NetworkCallback#FLAG_INCLUDE_LOCATION_INFO} to get the info in their
-     * callback. If the apps targets SDK version equal to {{@link Build.VERSION_CODES#R}, this field
-     * will always be included. The app will be blamed for location access if this field is
-     * included.
+     * {@link android.net.wifi.WifiNetworkSuggestion WifiNetworkSuggestion}), and only for the
+     * network they own. In the case of wifi network suggestors apps, this field is also location
+     * sensitive, so the app needs to hold {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
+     * permission. If the app targets SDK version greater than or equal to
+     * {@link Build.VERSION_CODES#S}, then they also need to use
+     * {@link NetworkCallback#FLAG_INCLUDE_LOCATION_INFO} to get the info in their callback. If the
+     * apps targets SDK version equal to {{@link Build.VERSION_CODES#R}, this field will always be
+     * included. The app will be blamed for location access if this field is included.
      * </p>
      */
     public int getOwnerUid() {
@@ -2129,14 +2128,17 @@
             sb.append(" SubscriptionIds: ").append(mSubIds);
         }
 
-        if (mUnderlyingNetworks != null && mUnderlyingNetworks.size() > 0) {
-            sb.append(" Underlying networks: [");
+        sb.append(" UnderlyingNetworks: ");
+        if (mUnderlyingNetworks != null) {
+            sb.append("[");
             final StringJoiner joiner = new StringJoiner(",");
             for (int i = 0; i < mUnderlyingNetworks.size(); i++) {
                 joiner.add(mUnderlyingNetworks.get(i).toString());
             }
             sb.append(joiner.toString());
             sb.append("]");
+        } else {
+            sb.append("Null");
         }
 
         sb.append("]");
diff --git a/framework/src/android/net/NetworkInfo.java b/framework/src/android/net/NetworkInfo.java
index bb23494..433933f 100644
--- a/framework/src/android/net/NetworkInfo.java
+++ b/framework/src/android/net/NetworkInfo.java
@@ -179,21 +179,19 @@
 
     /** {@hide} */
     @UnsupportedAppUsage
-    public NetworkInfo(NetworkInfo source) {
-        if (source != null) {
-            synchronized (source) {
-                mNetworkType = source.mNetworkType;
-                mSubtype = source.mSubtype;
-                mTypeName = source.mTypeName;
-                mSubtypeName = source.mSubtypeName;
-                mState = source.mState;
-                mDetailedState = source.mDetailedState;
-                mReason = source.mReason;
-                mExtraInfo = source.mExtraInfo;
-                mIsFailover = source.mIsFailover;
-                mIsAvailable = source.mIsAvailable;
-                mIsRoaming = source.mIsRoaming;
-            }
+    public NetworkInfo(@NonNull NetworkInfo source) {
+        synchronized (source) {
+            mNetworkType = source.mNetworkType;
+            mSubtype = source.mSubtype;
+            mTypeName = source.mTypeName;
+            mSubtypeName = source.mSubtypeName;
+            mState = source.mState;
+            mDetailedState = source.mDetailedState;
+            mReason = source.mReason;
+            mExtraInfo = source.mExtraInfo;
+            mIsFailover = source.mIsFailover;
+            mIsAvailable = source.mIsAvailable;
+            mIsRoaming = source.mIsRoaming;
         }
     }
 
@@ -479,7 +477,7 @@
      * @param detailedState the {@link DetailedState}.
      * @param reason a {@code String} indicating the reason for the state change,
      * if one was supplied. May be {@code null}.
-     * @param extraInfo an optional {@code String} providing addditional network state
+     * @param extraInfo an optional {@code String} providing additional network state
      * information passed up from the lower networking layers.
      * @deprecated Use {@link NetworkCapabilities} instead.
      */
@@ -491,6 +489,11 @@
             this.mState = stateMap.get(detailedState);
             this.mReason = reason;
             this.mExtraInfo = extraInfo;
+            // Catch both the case where detailedState is null and the case where it's some
+            // unknown value
+            if (null == mState) {
+                throw new NullPointerException("Unknown DetailedState : " + detailedState);
+            }
         }
     }
 
diff --git a/service/Android.bp b/service/Android.bp
index 911d67f..3ff7a7c 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -67,7 +67,8 @@
     static_libs: [
         "dnsresolver_aidl_interface-V9-java",
         "modules-utils-build",
-        "modules-utils-os",
+        "modules-utils-shell-command-handler",
+        "modules-utils-statemachine",
         "net-utils-device-common",
         "net-utils-device-common-netlink",
         "net-utils-framework-common",
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 408dba3..9e88890 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -229,6 +229,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.MessageUtils;
 import com.android.modules.utils.BasicShellCommandHandler;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
@@ -259,6 +260,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.io.Writer;
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -273,6 +275,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
@@ -1332,7 +1335,7 @@
         final NetworkRequest defaultInternetRequest = createDefaultRequest();
         mDefaultRequest = new NetworkRequestInfo(
                 Process.myUid(), defaultInternetRequest, null,
-                new Binder(), NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
+                null /* binder */, NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
                 null /* attributionTags */);
         mNetworkRequests.put(defaultInternetRequest, mDefaultRequest);
         mDefaultNetworkRequests.add(mDefaultRequest);
@@ -1552,7 +1555,7 @@
 
         if (enable) {
             handleRegisterNetworkRequest(new NetworkRequestInfo(
-                    Process.myUid(), networkRequest, null, new Binder(),
+                    Process.myUid(), networkRequest, null /* messenger */, null /* binder */,
                     NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
                     null /* attributionTags */));
         } else {
@@ -2032,6 +2035,7 @@
         if (!checkSettingsPermission(callerPid, callerUid)) {
             newNc.setUids(null);
             newNc.setSSID(null);
+            newNc.setUnderlyingNetworks(null);
         }
         if (newNc.getNetworkSpecifier() != null) {
             newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
@@ -3192,6 +3196,22 @@
         }
     }
 
+    private void dumpAllRequestInfoLogsToLogcat() {
+        try (PrintWriter logPw = new PrintWriter(new Writer() {
+            @Override
+            public void write(final char[] cbuf, final int off, final int len) {
+                // This method is called with 0-length and 1-length arrays for empty strings
+                // or strings containing only the DEL character.
+                if (len <= 1) return;
+                Log.e(TAG, new String(cbuf, off, len));
+            }
+            @Override public void flush() {}
+            @Override public void close() {}
+        })) {
+            mNetworkRequestInfoLogs.dump(logPw);
+        }
+    }
+
     /**
      * Return an array of all current NetworkAgentInfos sorted by network id.
      */
@@ -4020,6 +4040,20 @@
         return null;
     }
 
+    private void checkNrisConsistency(final NetworkRequestInfo nri) {
+        if (SdkLevel.isAtLeastT()) {
+            for (final NetworkRequestInfo n : mNetworkRequests.values()) {
+                if (n.mBinder != null && n.mBinder == nri.mBinder) {
+                    // Temporary help to debug b/194394697 ; TODO : remove this function when the
+                    // bug is fixed.
+                    dumpAllRequestInfoLogsToLogcat();
+                    throw new IllegalStateException("This NRI is already registered. New : " + nri
+                            + ", existing : " + n);
+                }
+            }
+        }
+    }
+
     private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) {
         final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj);
         // handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests.
@@ -4045,6 +4079,7 @@
         ensureRunningOnConnectivityServiceThread();
         for (final NetworkRequestInfo nri : nris) {
             mNetworkRequestInfoLogs.log("REGISTER " + nri);
+            checkNrisConsistency(nri);
             for (final NetworkRequest req : nri.mRequests) {
                 mNetworkRequests.put(req, nri);
                 // TODO: Consider update signal strength for other types.
@@ -4281,6 +4316,7 @@
         }
         nri.decrementRequestCount();
         mNetworkRequestInfoLogs.log("RELEASE " + nri);
+        checkNrisConsistency(nri);
 
         if (null != nri.getActiveRequest()) {
             if (!nri.getActiveRequest().isListen()) {
@@ -5918,7 +5954,12 @@
 
         void unlinkDeathRecipient() {
             if (null != mBinder) {
-                mBinder.unlinkToDeath(this, 0);
+                try {
+                    mBinder.unlinkToDeath(this, 0);
+                } catch (NoSuchElementException e) {
+                    // Temporary workaround for b/194394697 pending analysis of additional logs
+                    Log.wtf(TAG, "unlinkToDeath for already unlinked NRI " + this);
+                }
             }
         }
 
@@ -5938,7 +5979,7 @@
         @Override
         public void binderDied() {
             log("ConnectivityService NetworkRequestInfo binderDied(" +
-                    "uid/pid:" + mUid + "/" + mPid + ", " + mBinder + ")");
+                    "uid/pid:" + mUid + "/" + mPid + ", " + mRequests + ", " + mBinder + ")");
             // As an immutable collection, mRequests cannot change by the time the
             // lambda is evaluated on the handler thread so calling .get() from a binder thread
             // is acceptable. Use handleReleaseNetworkRequest and not directly
@@ -6060,7 +6101,7 @@
 
     @Override
     public NetworkRequest requestNetwork(int asUid, NetworkCapabilities networkCapabilities,
-            int reqTypeInt, Messenger messenger, int timeoutMs, IBinder binder,
+            int reqTypeInt, Messenger messenger, int timeoutMs, final IBinder binder,
             int legacyType, int callbackFlags, @NonNull String callingPackageName,
             @Nullable String callingAttributionTag) {
         if (legacyType != TYPE_NONE && !checkNetworkStackPermission()) {
@@ -7305,7 +7346,9 @@
         boolean suspended = true; // suspended if all underlying are suspended
 
         boolean hadUnderlyingNetworks = false;
+        ArrayList<Network> newUnderlyingNetworks = null;
         if (null != underlyingNetworks) {
+            newUnderlyingNetworks = new ArrayList<>();
             for (Network underlyingNetwork : underlyingNetworks) {
                 final NetworkAgentInfo underlying =
                         getNetworkAgentInfoForNetwork(underlyingNetwork);
@@ -7335,6 +7378,7 @@
                 // If this network is not suspended, the VPN is not suspended (the VPN
                 // is able to transfer some data).
                 suspended &= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+                newUnderlyingNetworks.add(underlyingNetwork);
             }
         }
         if (!hadUnderlyingNetworks) {
@@ -7352,6 +7396,7 @@
         newNc.setCapability(NET_CAPABILITY_NOT_ROAMING, !roaming);
         newNc.setCapability(NET_CAPABILITY_NOT_CONGESTED, !congested);
         newNc.setCapability(NET_CAPABILITY_NOT_SUSPENDED, !suspended);
+        newNc.setUnderlyingNetworks(newUnderlyingNetworks);
     }
 
     /**
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 6426f86..b7f3ed9 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -377,6 +377,9 @@
         this.creatorUid = creatorUid;
         mLingerDurationMs = lingerDurationMs;
         mQosCallbackTracker = qosCallbackTracker;
+        declaredUnderlyingNetworks = (nc.getUnderlyingNetworks() != null)
+                ? nc.getUnderlyingNetworks().toArray(new Network[0])
+                : null;
     }
 
     private class AgentDeathMonitor implements IBinder.DeathRecipient {
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 215f129..311b3f0 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -30,9 +30,18 @@
 import static android.system.OsConstants.SOCK_DGRAM;
 import static android.test.MoreAsserts.assertNotEqual;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
 import android.annotation.Nullable;
+import android.app.Activity;
 import android.app.DownloadManager;
 import android.app.DownloadManager.Query;
 import android.app.DownloadManager.Request;
@@ -71,15 +80,21 @@
 import android.system.Os;
 import android.system.OsConstants;
 import android.system.StructPollfd;
-import android.test.InstrumentationTestCase;
 import android.test.MoreAsserts;
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
 import com.android.compatibility.common.util.BlockingBroadcastReceiver;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.testutils.TestableNetworkCallback;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.Closeable;
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -127,7 +142,8 @@
  *   https://source.android.com/devices/tech/config/kernel_network_tests.html
  *
  */
-public class VpnTest extends InstrumentationTestCase {
+@RunWith(AndroidJUnit4.class)
+public class VpnTest {
 
     // These are neither public nor @TestApi.
     // TODO: add them to @TestApi.
@@ -161,17 +177,24 @@
         return !pm.hasSystemFeature("android.hardware.type.watch");
     }
 
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
+    public final <T extends Activity> T launchActivity(String packageName, Class<T> activityClass) {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setClassName(packageName, activityClass.getName());
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        final T activity = (T) getInstrumentation().startActivitySync(intent);
+        getInstrumentation().waitForIdleSync();
+        return activity;
+    }
 
+    @Before
+    public void setUp() throws Exception {
         mNetwork = null;
         mCallback = null;
         storePrivateDnsSetting();
 
         mDevice = UiDevice.getInstance(getInstrumentation());
         mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(),
-                MyActivity.class, null);
+                MyActivity.class);
         mPackageName = mActivity.getPackageName();
         mCM = (ConnectivityManager) mActivity.getSystemService(Context.CONNECTIVITY_SERVICE);
         mWifiManager = (WifiManager) mActivity.getSystemService(Context.WIFI_SERVICE);
@@ -180,7 +203,7 @@
         mDevice.waitForIdle();
     }
 
-    @Override
+    @After
     public void tearDown() throws Exception {
         restorePrivateDnsSetting();
         mRemoteSocketFactoryClient.unbind();
@@ -190,7 +213,6 @@
         Log.i(TAG, "Stopping VPN");
         stopVpn();
         mActivity.finish();
-        super.tearDown();
     }
 
     private void prepareVpn() throws Exception {
@@ -702,6 +724,7 @@
         setAndVerifyPrivateDns(initialMode);
     }
 
+    @Test
     public void testDefault() throws Exception {
         if (!supportedHardware()) return;
         // If adb TCP port opened, this test may running by adb over network.
@@ -762,6 +785,15 @@
         maybeExpectVpnTransportInfo(vpnNetwork);
         assertEquals(TYPE_VPN, mCM.getNetworkInfo(vpnNetwork).getType());
 
+        if (SdkLevel.isAtLeastT()) {
+            runWithShellPermissionIdentity(() -> {
+                final NetworkCapabilities nc = mCM.getNetworkCapabilities(vpnNetwork);
+                assertNotNull(nc);
+                assertNotNull(nc.getUnderlyingNetworks());
+                assertEquals(defaultNetwork, new ArrayList<>(nc.getUnderlyingNetworks()).get(0));
+            }, NETWORK_SETTINGS);
+        }
+
         if (SdkLevel.isAtLeastS()) {
             // Check that system default network callback has not seen any network changes, even
             // though the app's default network changed. Also check that otherUidCallback saw no
@@ -781,6 +813,7 @@
         receiver.unregisterQuietly();
     }
 
+    @Test
     public void testAppAllowed() throws Exception {
         if (!supportedHardware()) return;
 
@@ -801,6 +834,7 @@
         checkStrictModePrivateDns();
     }
 
+    @Test
     public void testAppDisallowed() throws Exception {
         if (!supportedHardware()) return;
 
@@ -829,6 +863,7 @@
         assertFalse(nc.hasTransport(TRANSPORT_VPN));
     }
 
+    @Test
     public void testGetConnectionOwnerUidSecurity() throws Exception {
         if (!supportedHardware()) return;
 
@@ -850,6 +885,7 @@
         }
     }
 
+    @Test
     public void testSetProxy() throws  Exception {
         if (!supportedHardware()) return;
         ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -889,6 +925,7 @@
         assertDefaultProxy(initialProxy);
     }
 
+    @Test
     public void testSetProxyDisallowedApps() throws Exception {
         if (!supportedHardware()) return;
         ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -908,6 +945,7 @@
         assertDefaultProxy(initialProxy);
     }
 
+    @Test
     public void testNoProxy() throws Exception {
         if (!supportedHardware()) return;
         ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -942,6 +980,7 @@
         assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork());
     }
 
+    @Test
     public void testBindToNetworkWithProxy() throws Exception {
         if (!supportedHardware()) return;
         String allowedApps = mPackageName;
@@ -966,6 +1005,7 @@
         assertDefaultProxy(initialProxy);
     }
 
+    @Test
     public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
         if (!supportedHardware()) {
             return;
@@ -986,8 +1026,18 @@
         assertTrue(mCM.isActiveNetworkMetered());
 
         maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+
+        if (SdkLevel.isAtLeastT()) {
+            runWithShellPermissionIdentity(() -> {
+                final NetworkCapabilities nc = mCM.getNetworkCapabilities(mNetwork);
+                assertNotNull(nc);
+                assertNotNull(nc.getUnderlyingNetworks());
+                assertEquals(underlyingNetworks, new ArrayList<>(nc.getUnderlyingNetworks()));
+            }, NETWORK_SETTINGS);
+        }
     }
 
+    @Test
     public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
         if (!supportedHardware()) {
             return;
@@ -1016,6 +1066,7 @@
         maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
     }
 
+    @Test
     public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
         if (!supportedHardware()) {
             return;
@@ -1043,8 +1094,21 @@
         assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
 
         maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+
+        if (SdkLevel.isAtLeastT()) {
+            final Network vpnNetwork = mCM.getActiveNetwork();
+            assertNotEqual(underlyingNetwork, vpnNetwork);
+            runWithShellPermissionIdentity(() -> {
+                final NetworkCapabilities nc = mCM.getNetworkCapabilities(vpnNetwork);
+                assertNotNull(nc);
+                assertNotNull(nc.getUnderlyingNetworks());
+                final List<Network> underlying = nc.getUnderlyingNetworks();
+                assertEquals(underlyingNetwork, underlying.get(0));
+            }, NETWORK_SETTINGS);
+        }
     }
 
+    @Test
     public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
         if (!supportedHardware()) {
             return;
@@ -1071,6 +1135,7 @@
         maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
     }
 
+    @Test
     public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
         if (!supportedHardware()) {
             return;
@@ -1096,8 +1161,21 @@
         assertTrue(mCM.isActiveNetworkMetered());
 
         maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+
+        if (SdkLevel.isAtLeastT()) {
+            final Network vpnNetwork = mCM.getActiveNetwork();
+            assertNotEqual(underlyingNetwork, vpnNetwork);
+            runWithShellPermissionIdentity(() -> {
+                final NetworkCapabilities nc = mCM.getNetworkCapabilities(vpnNetwork);
+                assertNotNull(nc);
+                assertNotNull(nc.getUnderlyingNetworks());
+                final List<Network> underlying = nc.getUnderlyingNetworks();
+                assertEquals(underlyingNetwork, underlying.get(0));
+            }, NETWORK_SETTINGS);
+        }
     }
 
+    @Test
     public void testB141603906() throws Exception {
         if (!supportedHardware()) {
             return;
@@ -1176,7 +1254,7 @@
         private boolean received;
 
         public ProxyChangeBroadcastReceiver() {
-            super(VpnTest.this.getInstrumentation().getContext(), Proxy.PROXY_CHANGE_ACTION);
+            super(getInstrumentation().getContext(), Proxy.PROXY_CHANGE_ACTION);
             received = false;
         }
 
@@ -1196,6 +1274,7 @@
      * allowed list.
      * See b/165774987.
      */
+    @Test
     public void testDownloadWithDownloadManagerDisallowed() throws Exception {
         if (!supportedHardware()) return;
 
@@ -1205,7 +1284,7 @@
                 "" /* allowedApps */, "com.android.providers.downloads", null /* proxyInfo */,
                 null /* underlyingNetworks */, false /* isAlwaysMetered */);
 
-        final Context context = VpnTest.this.getInstrumentation().getContext();
+        final Context context = getInstrumentation().getContext();
         final DownloadManager dm = context.getSystemService(DownloadManager.class);
         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
         try {
diff --git a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
index 80951ca..6b2a1ee 100644
--- a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
@@ -41,11 +41,11 @@
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
 
+import androidx.test.filters.RequiresDevice;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.SkipPresubmit;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -94,7 +94,7 @@
     // properly.
     @Test
     @AppModeFull(reason = "Cannot get CHANGE_NETWORK_STATE to request wifi/cell in instant mode")
-    @SkipPresubmit(reason = "Virtual hardware does not support wifi battery stats")
+    @RequiresDevice // Virtual hardware does not support wifi battery stats
     public void testReportNetworkInterfaceForTransports() throws Exception {
         try {
             // Simulate the device being unplugged from charging.
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 4329a83..579be15 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -155,6 +155,7 @@
 import android.util.Range;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.RequiresDevice;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.ArrayUtils;
@@ -168,7 +169,6 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRuleKt;
 import com.android.testutils.RecorderCallback.CallbackEntry;
-import com.android.testutils.SkipPresubmit;
 import com.android.testutils.TestHttpServer;
 import com.android.testutils.TestNetworkTracker;
 import com.android.testutils.TestableNetworkCallback;
@@ -559,7 +559,7 @@
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
-    @SkipPresubmit(reason = "Virtual devices use a single internet connection for all networks")
+    @RequiresDevice // Virtual devices use a single internet connection for all networks
     public void testOpenConnection() throws Exception {
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
@@ -714,6 +714,12 @@
                 .build();
     }
 
+    private boolean hasPrivateDnsValidated(CallbackEntry entry, Network networkForPrivateDns) {
+        if (!networkForPrivateDns.equals(entry.getNetwork())) return false;
+        final NetworkCapabilities nc = ((CallbackEntry.CapabilitiesChanged) entry).getCaps();
+        return !nc.isPrivateDnsBroken() && nc.hasCapability(NET_CAPABILITY_VALIDATED);
+    }
+
     @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testIsPrivateDnsBroken() throws InterruptedException {
@@ -727,8 +733,7 @@
             mCtsNetUtils.setPrivateDnsStrictMode(goodPrivateDnsServer);
             final Network networkForPrivateDns =  mCtsNetUtils.ensureWifiConnected();
             cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
-                    entry -> (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
+                    entry -> hasPrivateDnsValidated(entry, networkForPrivateDns));
 
             // Verifying the broken private DNS sever
             mCtsNetUtils.setPrivateDnsStrictMode(invalidPrivateDnsServer);
@@ -1183,6 +1188,7 @@
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testGetMultipathPreference() throws Exception {
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
         final ContentResolver resolver = mContext.getContentResolver();
         mCtsNetUtils.ensureWifiConnected();
         final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
@@ -1419,7 +1425,7 @@
 
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
-    @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+    @RequiresDevice // Keepalive is not supported on virtual hardware
     public void testCreateTcpKeepalive() throws Exception {
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
@@ -1626,7 +1632,7 @@
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
-    @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+    @RequiresDevice // Keepalive is not supported on virtual hardware
     public void testSocketKeepaliveLimitWifi() throws Exception {
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
@@ -1676,7 +1682,7 @@
      */
     @AppModeFull(reason = "Cannot request network in instant app mode")
     @Test
-    @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+    @RequiresDevice // Keepalive is not supported on virtual hardware
     public void testSocketKeepaliveLimitTelephony() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
             Log.i(TAG, "testSocketKeepaliveLimitTelephony cannot execute unless device"
@@ -1722,7 +1728,7 @@
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
-    @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+    @RequiresDevice // Keepalive is not supported on virtual hardware
     public void testSocketKeepaliveUnprivileged() throws Exception {
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
@@ -2828,6 +2834,19 @@
             });
     }
 
+    /**
+     *  The networks used in this test are real networks and as such they can see seemingly random
+     *  updates of their capabilities or link properties as conditions change, e.g. the network
+     *  loses validation or IPv4 shows up. Many tests should simply treat these callbacks as
+     *  spurious.
+     */
+    private void assertNoCallbackExceptCapOrLpChange(
+            @NonNull final TestableNetworkCallback cb) {
+        cb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS,
+                c -> !(c instanceof CallbackEntry.CapabilitiesChanged
+                        || c instanceof CallbackEntry.LinkPropertiesChanged));
+    }
+
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testMobileDataPreferredUids() throws Exception {
@@ -2860,8 +2879,7 @@
             // CtsNetTestCases uid is not listed in MOBILE_DATA_PREFERRED_UIDS setting, so the
             // per-app default network should be same as system default network.
             waitForAvailable(systemDefaultCb, wifiNetwork);
-            defaultTrackingCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
-                    entry -> wifiNetwork.equals(entry.getNetwork()));
+            waitForAvailable(defaultTrackingCb, wifiNetwork);
             // Active network for CtsNetTestCases uid should be wifi now.
             assertEquals(wifiNetwork, mCm.getActiveNetwork());
 
@@ -2871,10 +2889,10 @@
             newMobileDataPreferredUids.add(uid);
             ConnectivitySettingsManager.setMobileDataPreferredUids(
                     mContext, newMobileDataPreferredUids);
-            defaultTrackingCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
-                    entry -> cellNetwork.equals(entry.getNetwork()));
-            // System default network doesn't change.
-            systemDefaultCb.assertNoCallback();
+            waitForAvailable(defaultTrackingCb, cellNetwork);
+            // No change for system default network. Expect no callback except CapabilitiesChanged
+            // or LinkPropertiesChanged which may be triggered randomly from wifi network.
+            assertNoCallbackExceptCapOrLpChange(systemDefaultCb);
             // Active network for CtsNetTestCases uid should change to cell, too.
             assertEquals(cellNetwork, mCm.getActiveNetwork());
 
@@ -2883,10 +2901,10 @@
             newMobileDataPreferredUids.remove(uid);
             ConnectivitySettingsManager.setMobileDataPreferredUids(
                     mContext, newMobileDataPreferredUids);
-            defaultTrackingCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
-                    entry -> wifiNetwork.equals(entry.getNetwork()));
-            // System default network still doesn't change.
-            systemDefaultCb.assertNoCallback();
+            waitForAvailable(defaultTrackingCb, wifiNetwork);
+            // No change for system default network. Expect no callback except CapabilitiesChanged
+            // or LinkPropertiesChanged which may be triggered randomly from wifi network.
+            assertNoCallbackExceptCapOrLpChange(systemDefaultCb);
             // Active network for CtsNetTestCases uid should change back to wifi.
             assertEquals(wifiNetwork, mCm.getActiveNetwork());
         } finally {
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index fde27e9..fb63a19 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -16,7 +16,6 @@
 
 package android.net.cts;
 
-import android.content.Context;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
@@ -27,7 +26,7 @@
 import android.test.AndroidTestCase;
 import android.util.Log;
 
-import com.android.testutils.SkipPresubmit;
+import androidx.test.filters.RequiresDevice;
 
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -70,7 +69,7 @@
      * Perf - measure size of first and second tier caches and their effect
      * Assert requires network permission
      */
-    @SkipPresubmit(reason = "IPv6 support may be missing on presubmit virtual hardware")
+    @RequiresDevice // IPv6 support may be missing on presubmit virtual hardware
     public void testDnsWorks() throws Exception {
         ensureIpv6Connectivity();
 
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index f963593..b1ffc92 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -16,20 +16,38 @@
 
 package android.net
 
+import android.content.Context
+import android.net.ConnectivityManager.TYPE_MOBILE
 import android.net.NetworkIdentity.OEM_NONE
 import android.net.NetworkIdentity.OEM_PAID
 import android.net.NetworkIdentity.OEM_PRIVATE
 import android.net.NetworkIdentity.getOemBitfield
+import android.telephony.TelephonyManager
 import android.os.Build
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
 import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+private const val TEST_IMSI = "testimsi"
 
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class NetworkIdentityTest {
+    private val mockContext = mock(Context::class.java)
+
+    private fun buildMobileNetworkStateSnapshot(
+        caps: NetworkCapabilities,
+        subscriberId: String
+    ): NetworkStateSnapshot {
+        return NetworkStateSnapshot(mock(Network::class.java), caps,
+                LinkProperties(), subscriberId, TYPE_MOBILE)
+    }
+
     @Test
     fun testGetOemBitfield() {
         val oemNone = NetworkCapabilities().apply {
@@ -54,4 +72,32 @@
         assertEquals(getOemBitfield(oemPrivate), OEM_PRIVATE)
         assertEquals(getOemBitfield(oemAll), OEM_PAID or OEM_PRIVATE)
     }
+
+    @Test
+    fun testGetMetered() {
+        // Verify network is metered.
+        val netIdent1 = NetworkIdentity.buildNetworkIdentity(mockContext,
+                buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI),
+                false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        assertTrue(netIdent1.getMetered())
+
+        // Verify network is not metered because it has NET_CAPABILITY_NOT_METERED capability.
+        val capsNotMetered = NetworkCapabilities.Builder().apply {
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+        }.build()
+        val netIdent2 = NetworkIdentity.buildNetworkIdentity(mockContext,
+                buildMobileNetworkStateSnapshot(capsNotMetered, TEST_IMSI),
+                false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        assertFalse(netIdent2.getMetered())
+
+        // Verify network is not metered because it has NET_CAPABILITY_TEMPORARILY_NOT_METERED
+        // capability .
+        val capsTempNotMetered = NetworkCapabilities().apply {
+            setCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED, true)
+        }
+        val netIdent3 = NetworkIdentity.buildNetworkIdentity(mockContext,
+                buildMobileNetworkStateSnapshot(capsTempNotMetered, TEST_IMSI),
+                false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        assertFalse(netIdent3.getMetered())
+    }
 }
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 9ff594a..2db77f2 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -26,6 +26,8 @@
 import android.net.NetworkIdentity.buildNetworkIdentity
 import android.net.NetworkStats.DEFAULT_NETWORK_ALL
 import android.net.NetworkStats.METERED_ALL
+import android.net.NetworkStats.METERED_NO
+import android.net.NetworkStats.METERED_YES
 import android.net.NetworkStats.ROAMING_ALL
 import android.net.NetworkTemplate.MATCH_MOBILE
 import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
@@ -176,7 +178,7 @@
     fun testMobileMatches() {
         val templateMobileImsi1 = buildTemplateMobileAll(TEST_IMSI1)
         val templateMobileImsi2WithRatType = buildTemplateMobileWithRatType(TEST_IMSI2,
-                TelephonyManager.NETWORK_TYPE_UMTS)
+                TelephonyManager.NETWORK_TYPE_UMTS, METERED_YES)
 
         val mobileImsi1 = buildNetworkState(TYPE_MOBILE, TEST_IMSI1, null /* ssid */,
                 OEM_NONE, true /* metered */)
@@ -205,7 +207,7 @@
     fun testMobileWildcardMatches() {
         val templateMobileWildcard = buildTemplateMobileWildcard()
         val templateMobileNullImsiWithRatType = buildTemplateMobileWithRatType(null,
-                TelephonyManager.NETWORK_TYPE_UMTS)
+                TelephonyManager.NETWORK_TYPE_UMTS, METERED_ALL)
 
         val mobileImsi1 = buildMobileNetworkState(TEST_IMSI1)
         val identMobile1 = buildNetworkIdentity(mockContext, mobileImsi1,
@@ -258,58 +260,131 @@
         templateCarrierImsi1Metered.assertDoesNotMatch(identCarrierWifiImsi1NonMetered)
     }
 
+    // TODO: Refactor this test to reduce the line of codes.
     @Test
     fun testRatTypeGroupMatches() {
-        val stateMobile = buildMobileNetworkState(TEST_IMSI1)
+        val stateMobileImsi1Metered = buildMobileNetworkState(TEST_IMSI1)
+        val stateMobileImsi1NonMetered = buildNetworkState(TYPE_MOBILE, TEST_IMSI1,
+                null /* ssid */, OEM_NONE, false /* metered */)
+        val stateMobileImsi2NonMetered = buildNetworkState(TYPE_MOBILE, TEST_IMSI2,
+                null /* ssid */, OEM_NONE, false /* metered */)
+
         // Build UMTS template that matches mobile identities with RAT in the same
         // group with any IMSI. See {@link NetworkTemplate#getCollapsedRatType}.
-        val templateUmts = buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS)
+        val templateUmtsMetered = buildTemplateMobileWithRatType(null,
+                TelephonyManager.NETWORK_TYPE_UMTS, METERED_YES)
         // Build normal template that matches mobile identities with any RAT and IMSI.
-        val templateAll = buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL)
+        val templateAllMetered = buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL,
+                METERED_YES)
         // Build template with UNKNOWN RAT that matches mobile identities with RAT that
         // cannot be determined.
-        val templateUnknown =
-                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN)
+        val templateUnknownMetered =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                METERED_YES)
 
-        val identUmts = buildNetworkIdentity(
-                mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_UMTS)
-        val identHsdpa = buildNetworkIdentity(
-                mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_HSDPA)
-        val identLte = buildNetworkIdentity(
-                mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_LTE)
-        val identCombined = buildNetworkIdentity(
-                mockContext, stateMobile, false, SUBTYPE_COMBINED)
-        val identImsi2 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI2),
-                false, TelephonyManager.NETWORK_TYPE_UMTS)
+        val templateUmtsNonMetered = buildTemplateMobileWithRatType(null,
+                TelephonyManager.NETWORK_TYPE_UMTS, METERED_NO)
+        val templateAllNonMetered = buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL,
+                METERED_NO)
+        val templateUnknownNonMetered =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                METERED_NO)
+
+        val identUmtsMetered = buildNetworkIdentity(
+                mockContext, stateMobileImsi1Metered, false, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identHsdpaMetered = buildNetworkIdentity(
+                mockContext, stateMobileImsi1Metered, false, TelephonyManager.NETWORK_TYPE_HSDPA)
+        val identLteMetered = buildNetworkIdentity(
+                mockContext, stateMobileImsi1Metered, false, TelephonyManager.NETWORK_TYPE_LTE)
+        val identCombinedMetered = buildNetworkIdentity(
+                mockContext, stateMobileImsi1Metered, false, SUBTYPE_COMBINED)
+        val identImsi2UmtsMetered = buildNetworkIdentity(mockContext,
+                buildMobileNetworkState(TEST_IMSI2), false, TelephonyManager.NETWORK_TYPE_UMTS)
         val identWifi = buildNetworkIdentity(
                 mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0)
 
-        // Assert that identity with the same RAT matches.
-        templateUmts.assertMatches(identUmts)
-        templateAll.assertMatches(identUmts)
-        templateUnknown.assertDoesNotMatch(identUmts)
+        val identUmtsNonMetered = buildNetworkIdentity(
+                mockContext, stateMobileImsi1NonMetered, false, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identHsdpaNonMetered = buildNetworkIdentity(
+                mockContext, stateMobileImsi1NonMetered, false,
+                TelephonyManager.NETWORK_TYPE_HSDPA)
+        val identLteNonMetered = buildNetworkIdentity(
+                mockContext, stateMobileImsi1NonMetered, false, TelephonyManager.NETWORK_TYPE_LTE)
+        val identCombinedNonMetered = buildNetworkIdentity(
+                mockContext, stateMobileImsi1NonMetered, false, SUBTYPE_COMBINED)
+        val identImsi2UmtsNonMetered = buildNetworkIdentity(mockContext,
+                stateMobileImsi2NonMetered, false, TelephonyManager.NETWORK_TYPE_UMTS)
+
+        // Assert that identity with the same RAT and meteredness matches.
+        // Verify metered template.
+        templateUmtsMetered.assertMatches(identUmtsMetered)
+        templateAllMetered.assertMatches(identUmtsMetered)
+        templateUnknownMetered.assertDoesNotMatch(identUmtsMetered)
+        // Verify non-metered template.
+        templateUmtsNonMetered.assertMatches(identUmtsNonMetered)
+        templateAllNonMetered.assertMatches(identUmtsNonMetered)
+        templateUnknownNonMetered.assertDoesNotMatch(identUmtsNonMetered)
+
+        // Assert that identity with the same RAT but meteredness is different.
+        // Thus, it does not match.
+        templateUmtsNonMetered.assertDoesNotMatch(identUmtsMetered)
+        templateAllNonMetered.assertDoesNotMatch(identUmtsMetered)
+
         // Assert that identity with the RAT within the same group matches.
-        templateUmts.assertMatches(identHsdpa)
-        templateAll.assertMatches(identHsdpa)
-        templateUnknown.assertDoesNotMatch(identHsdpa)
+        // Verify metered template.
+        templateUmtsMetered.assertMatches(identHsdpaMetered)
+        templateAllMetered.assertMatches(identHsdpaMetered)
+        templateUnknownMetered.assertDoesNotMatch(identHsdpaMetered)
+        // Verify non-metered template.
+        templateUmtsNonMetered.assertMatches(identHsdpaNonMetered)
+        templateAllNonMetered.assertMatches(identHsdpaNonMetered)
+        templateUnknownNonMetered.assertDoesNotMatch(identHsdpaNonMetered)
+
         // Assert that identity with the RAT out of the same group only matches template with
         // NETWORK_TYPE_ALL.
-        templateUmts.assertDoesNotMatch(identLte)
-        templateAll.assertMatches(identLte)
-        templateUnknown.assertDoesNotMatch(identLte)
+        // Verify metered template.
+        templateUmtsMetered.assertDoesNotMatch(identLteMetered)
+        templateAllMetered.assertMatches(identLteMetered)
+        templateUnknownMetered.assertDoesNotMatch(identLteMetered)
+        // Verify non-metered template.
+        templateUmtsNonMetered.assertDoesNotMatch(identLteNonMetered)
+        templateAllNonMetered.assertMatches(identLteNonMetered)
+        templateUnknownNonMetered.assertDoesNotMatch(identLteNonMetered)
+        // Verify non-metered template does not match identity with metered.
+        templateAllNonMetered.assertDoesNotMatch(identLteMetered)
+
         // Assert that identity with combined RAT only matches with template with NETWORK_TYPE_ALL
         // and NETWORK_TYPE_UNKNOWN.
-        templateUmts.assertDoesNotMatch(identCombined)
-        templateAll.assertMatches(identCombined)
-        templateUnknown.assertMatches(identCombined)
+        // Verify metered template.
+        templateUmtsMetered.assertDoesNotMatch(identCombinedMetered)
+        templateAllMetered.assertMatches(identCombinedMetered)
+        templateUnknownMetered.assertMatches(identCombinedMetered)
+        // Verify non-metered template.
+        templateUmtsNonMetered.assertDoesNotMatch(identCombinedNonMetered)
+        templateAllNonMetered.assertMatches(identCombinedNonMetered)
+        templateUnknownNonMetered.assertMatches(identCombinedNonMetered)
+        // Verify that identity with metered does not match non-metered template.
+        templateAllNonMetered.assertDoesNotMatch(identCombinedMetered)
+        templateUnknownNonMetered.assertDoesNotMatch(identCombinedMetered)
+
         // Assert that identity with different IMSI matches.
-        templateUmts.assertMatches(identImsi2)
-        templateAll.assertMatches(identImsi2)
-        templateUnknown.assertDoesNotMatch(identImsi2)
+        // Verify metered template.
+        templateUmtsMetered.assertMatches(identImsi2UmtsMetered)
+        templateAllMetered.assertMatches(identImsi2UmtsMetered)
+        templateUnknownMetered.assertDoesNotMatch(identImsi2UmtsMetered)
+        // Verify non-metered template.
+        templateUmtsNonMetered.assertMatches(identImsi2UmtsNonMetered)
+        templateAllNonMetered.assertMatches(identImsi2UmtsNonMetered)
+        templateUnknownNonMetered.assertDoesNotMatch(identImsi2UmtsNonMetered)
+        // Verify that the same RAT but different meteredness should not match.
+        templateUmtsNonMetered.assertDoesNotMatch(identImsi2UmtsMetered)
+        templateAllNonMetered.assertDoesNotMatch(identImsi2UmtsMetered)
+
         // Assert that wifi identity does not match.
-        templateUmts.assertDoesNotMatch(identWifi)
-        templateAll.assertDoesNotMatch(identWifi)
-        templateUnknown.assertDoesNotMatch(identWifi)
+        templateUmtsMetered.assertDoesNotMatch(identWifi)
+        templateUnknownMetered.assertDoesNotMatch(identWifi)
+        templateUmtsNonMetered.assertDoesNotMatch(identWifi)
+        templateUnknownNonMetered.assertDoesNotMatch(identWifi)
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 352a468..bc50d0d 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -151,6 +151,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.AdditionalMatchers.aryEq;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -158,7 +159,6 @@
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.ArgumentMatchers.startsWith;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
@@ -313,17 +313,20 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.connectivity.resources.R;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
 import com.android.internal.util.WakeupMessage;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.LocationPermissionChecker;
 import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
 import com.android.server.ConnectivityService.NetworkRequestInfo;
+import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
 import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.connectivity.Nat464Xlat;
 import com.android.server.connectivity.NetworkAgentInfo;
@@ -350,11 +353,8 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
-import org.mockito.MockingDetails;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
-import org.mockito.exceptions.misusing.UnfinishedStubbingException;
 import org.mockito.stubbing.Answer;
 
 import java.io.FileDescriptor;
@@ -467,7 +467,7 @@
     private MockContext mServiceContext;
     private HandlerThread mCsHandlerThread;
     private HandlerThread mVMSHandlerThread;
-    private ConnectivityService.Dependencies mDeps;
+    private ConnectivityServiceDependencies mDeps;
     private ConnectivityService mService;
     private WrappedConnectivityManager mCm;
     private TestNetworkAgentWrapper mWiFiNetworkAgent;
@@ -505,7 +505,6 @@
     @Mock LocationManager mLocationManager;
     @Mock AppOpsManager mAppOpsManager;
     @Mock TelephonyManager mTelephonyManager;
-    @Mock MockableSystemProperties mSystemProperties;
     @Mock EthernetManager mEthernetManager;
     @Mock NetworkPolicyManager mNetworkPolicyManager;
     @Mock VpnProfileStore mVpnProfileStore;
@@ -1580,11 +1579,11 @@
     }
 
     private <T> T doAsUid(final int uid, @NonNull final Supplier<T> what) {
-        doReturn(uid).when(mDeps).getCallingUid();
+        mDeps.setCallingUid(uid);
         try {
             return what.get();
         } finally {
-            returnRealCallingUid();
+            mDeps.setCallingUid(null);
         }
     }
 
@@ -1703,8 +1702,13 @@
 
         mCsHandlerThread = new HandlerThread("TestConnectivityService");
         mVMSHandlerThread = new HandlerThread("TestVpnManagerService");
-        mDeps = makeDependencies();
-        returnRealCallingUid();
+
+        initMockedResources();
+        final Context mockResContext = mock(Context.class);
+        doReturn(mResources).when(mockResContext).getResources();
+        ConnectivityResources.setResourcesContextForTest(mockResContext);
+        mDeps = new ConnectivityServiceDependencies(mockResContext);
+
         mService = new ConnectivityService(mServiceContext,
                 mMockDnsResolver,
                 mock(IpConnectivityLog.class),
@@ -1712,7 +1716,6 @@
                 mDeps);
         mService.mLingerDelayMs = TEST_LINGER_DELAY_MS;
         mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS;
-        verify(mDeps).makeMultinetworkPolicyTracker(any(), any(), any());
 
         final ArgumentCaptor<NetworkPolicyCallback> policyCallbackCaptor =
                 ArgumentCaptor.forClass(NetworkPolicyCallback.class);
@@ -1736,41 +1739,7 @@
         setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
     }
 
-    private void returnRealCallingUid() {
-        try {
-            doAnswer((invocationOnMock) -> Binder.getCallingUid()).when(mDeps).getCallingUid();
-        } catch (UnfinishedStubbingException e) {
-            final MockingDetails details = Mockito.mockingDetails(mDeps);
-            Log.e("ConnectivityServiceTest", "UnfinishedStubbingException,"
-                    + " Stubbings: " + TextUtils.join(", ", details.getStubbings())
-                    + " Invocations: " + details.printInvocations(), e);
-            throw e;
-        }
-    }
-
-    private ConnectivityService.Dependencies makeDependencies() {
-        doReturn(false).when(mSystemProperties).getBoolean("ro.radio.noril", false);
-        final ConnectivityService.Dependencies deps = mock(ConnectivityService.Dependencies.class);
-        doReturn(mCsHandlerThread).when(deps).makeHandlerThread();
-        doReturn(mNetIdManager).when(deps).makeNetIdManager();
-        doReturn(mNetworkStack).when(deps).getNetworkStack();
-        doReturn(mSystemProperties).when(deps).getSystemProperties();
-        doReturn(mProxyTracker).when(deps).makeProxyTracker(any(), any());
-        doReturn(true).when(deps).queryUserAccess(anyInt(), any(), any());
-        doAnswer(inv -> {
-            mPolicyTracker = new WrappedMultinetworkPolicyTracker(
-                    inv.getArgument(0), inv.getArgument(1), inv.getArgument(2));
-            return mPolicyTracker;
-        }).when(deps).makeMultinetworkPolicyTracker(any(), any(), any());
-        doReturn(true).when(deps).getCellular464XlatEnabled();
-        doAnswer(inv ->
-            new LocationPermissionChecker(inv.getArgument(0)) {
-                @Override
-                protected int getCurrentUser() {
-                    return runAsShell(CREATE_USERS, () -> super.getCurrentUser());
-                }
-            }).when(deps).makeLocationPermissionChecker(any());
-
+    private void initMockedResources() {
         doReturn(60000).when(mResources).getInteger(R.integer.config_networkTransitionTimeout);
         doReturn("").when(mResources).getString(R.string.config_networkCaptivePortalServerUrl);
         doReturn(new String[]{ WIFI_WOL_IFNAME }).when(mResources).getStringArray(
@@ -1783,7 +1752,8 @@
                 R.array.config_protectedNetworks);
         // We don't test the actual notification value strings, so just return an empty array.
         // It doesn't matter what the values are as long as it's not null.
-        doReturn(new String[0]).when(mResources).getStringArray(R.array.network_switch_type_name);
+        doReturn(new String[0]).when(mResources)
+                .getStringArray(R.array.network_switch_type_name);
 
         doReturn(R.array.config_networkSupportedKeepaliveCount).when(mResources)
                 .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any());
@@ -1794,22 +1764,158 @@
         doReturn(1).when(mResources).getInteger(R.integer.config_networkAvoidBadWifi);
         doReturn(true).when(mResources)
                 .getBoolean(R.bool.config_cellular_radio_timesharing_capable);
+    }
 
-        final ConnectivityResources connRes = mock(ConnectivityResources.class);
-        doReturn(mResources).when(connRes).get();
-        doReturn(connRes).when(deps).getResources(any());
+    class ConnectivityServiceDependencies extends ConnectivityService.Dependencies {
+        final ConnectivityResources mConnRes;
+        @Mock final MockableSystemProperties mSystemProperties;
 
-        final Context mockResContext = mock(Context.class);
-        doReturn(mResources).when(mockResContext).getResources();
-        ConnectivityResources.setResourcesContextForTest(mockResContext);
+        ConnectivityServiceDependencies(final Context mockResContext) {
+            mSystemProperties = mock(MockableSystemProperties.class);
+            doReturn(false).when(mSystemProperties).getBoolean("ro.radio.noril", false);
 
-        doAnswer(inv -> {
-            final PendingIntent a = inv.getArgument(0);
-            final PendingIntent b = inv.getArgument(1);
+            mConnRes = new ConnectivityResources(mockResContext);
+        }
+
+        @Override
+        public MockableSystemProperties getSystemProperties() {
+            return mSystemProperties;
+        }
+
+        @Override
+        public HandlerThread makeHandlerThread() {
+            return mCsHandlerThread;
+        }
+
+        @Override
+        public NetworkStackClientBase getNetworkStack() {
+            return mNetworkStack;
+        }
+
+        @Override
+        public ProxyTracker makeProxyTracker(final Context context, final Handler handler) {
+            return mProxyTracker;
+        }
+
+        @Override
+        public NetIdManager makeNetIdManager() {
+            return mNetIdManager;
+        }
+
+        @Override
+        public boolean queryUserAccess(final int uid, final Network network,
+                final ConnectivityService cs) {
+            return true;
+        }
+
+        @Override
+        public MultinetworkPolicyTracker makeMultinetworkPolicyTracker(final Context c,
+                final Handler h, final Runnable r) {
+            if (null != mPolicyTracker) {
+                throw new IllegalStateException("Multinetwork policy tracker already initialized");
+            }
+            mPolicyTracker = new WrappedMultinetworkPolicyTracker(mServiceContext, h, r);
+            return mPolicyTracker;
+        }
+
+        @Override
+        public ConnectivityResources getResources(final Context ctx) {
+            return mConnRes;
+        }
+
+        @Override
+        public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+            return new LocationPermissionChecker(context) {
+                @Override
+                protected int getCurrentUser() {
+                    return runAsShell(CREATE_USERS, () -> super.getCurrentUser());
+                }
+            };
+        }
+
+        @Override
+        public boolean intentFilterEquals(final PendingIntent a, final PendingIntent b) {
             return runAsShell(GET_INTENT_SENDER_INTENT, () -> a.intentFilterEquals(b));
-        }).when(deps).intentFilterEquals(any(), any());
+        }
 
-        return deps;
+        @GuardedBy("this")
+        private Integer mCallingUid = null;
+
+        @Override
+        public int getCallingUid() {
+            synchronized (this) {
+                if (null != mCallingUid) return mCallingUid;
+                return super.getCallingUid();
+            }
+        }
+
+        // Pass null for the real calling UID
+        public void setCallingUid(final Integer uid) {
+            synchronized (this) {
+                mCallingUid = uid;
+            }
+        }
+
+        @GuardedBy("this")
+        private boolean mCellular464XlatEnabled = true;
+
+        @Override
+        public boolean getCellular464XlatEnabled() {
+            synchronized (this) {
+                return mCellular464XlatEnabled;
+            }
+        }
+
+        public void setCellular464XlatEnabled(final boolean enabled) {
+            synchronized (this) {
+                mCellular464XlatEnabled = enabled;
+            }
+        }
+
+        @GuardedBy("this")
+        private Integer mConnectionOwnerUid = null;
+
+        @Override
+        public int getConnectionOwnerUid(final int protocol, final InetSocketAddress local,
+                final InetSocketAddress remote) {
+            synchronized (this) {
+                if (null != mConnectionOwnerUid) return mConnectionOwnerUid;
+                return super.getConnectionOwnerUid(protocol, local, remote);
+            }
+        }
+
+        // Pass null to get the production implementation of getConnectionOwnerUid
+        public void setConnectionOwnerUid(final Integer uid) {
+            synchronized (this) {
+                mConnectionOwnerUid = uid;
+            }
+        }
+
+        final class ReportedInterfaces {
+            public final Context context;
+            public final String iface;
+            public final int[] transportTypes;
+            ReportedInterfaces(final Context c, final String i, final int[] t) {
+                context = c;
+                iface = i;
+                transportTypes = t;
+            }
+
+            public boolean contentEquals(final Context c, final String i, final int[] t) {
+                return Objects.equals(context, c) && Objects.equals(iface, i)
+                        && Arrays.equals(transportTypes, t);
+            }
+        }
+
+        final ArrayTrackRecord<ReportedInterfaces> mReportedInterfaceHistory =
+                new ArrayTrackRecord<>();
+
+        @Override
+        public void reportNetworkInterfaceForTransports(final Context context, final String iface,
+                final int[] transportTypes) {
+            mReportedInterfaceHistory.add(new ReportedInterfaces(context, iface, transportTypes));
+            super.reportNetworkInterfaceForTransports(context, iface, transportTypes);
+        }
     }
 
     private static void initAlarmManager(final AlarmManager am, final Handler alarmHandler) {
@@ -5134,9 +5240,6 @@
 
     @Test
     public void testAvoidBadWifiSetting() throws Exception {
-        final ContentResolver cr = mServiceContext.getContentResolver();
-        final String settingName = ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI;
-
         doReturn(1).when(mResources).getInteger(R.integer.config_networkAvoidBadWifi);
         testAvoidBadWifiConfig_ignoreSettings();
 
@@ -7252,6 +7355,15 @@
         initialCaps.addTransportType(TRANSPORT_VPN);
         initialCaps.addCapability(NET_CAPABILITY_INTERNET);
         initialCaps.removeCapability(NET_CAPABILITY_NOT_VPN);
+        final ArrayList<Network> emptyUnderlyingNetworks = new ArrayList<Network>();
+        final ArrayList<Network> underlyingNetworksContainMobile = new ArrayList<Network>();
+        underlyingNetworksContainMobile.add(mobile);
+        final ArrayList<Network> underlyingNetworksContainWifi = new ArrayList<Network>();
+        underlyingNetworksContainWifi.add(wifi);
+        final ArrayList<Network> underlyingNetworksContainMobileAndMobile =
+                new ArrayList<Network>();
+        underlyingNetworksContainMobileAndMobile.add(mobile);
+        underlyingNetworksContainMobileAndMobile.add(wifi);
 
         final NetworkCapabilities withNoUnderlying = new NetworkCapabilities();
         withNoUnderlying.addCapability(NET_CAPABILITY_INTERNET);
@@ -7260,17 +7372,20 @@
         withNoUnderlying.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
         withNoUnderlying.addTransportType(TRANSPORT_VPN);
         withNoUnderlying.removeCapability(NET_CAPABILITY_NOT_VPN);
+        withNoUnderlying.setUnderlyingNetworks(emptyUnderlyingNetworks);
 
         final NetworkCapabilities withMobileUnderlying = new NetworkCapabilities(withNoUnderlying);
         withMobileUnderlying.addTransportType(TRANSPORT_CELLULAR);
         withMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_ROAMING);
         withMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_SUSPENDED);
         withMobileUnderlying.setLinkDownstreamBandwidthKbps(10);
+        withMobileUnderlying.setUnderlyingNetworks(underlyingNetworksContainMobile);
 
         final NetworkCapabilities withWifiUnderlying = new NetworkCapabilities(withNoUnderlying);
         withWifiUnderlying.addTransportType(TRANSPORT_WIFI);
         withWifiUnderlying.addCapability(NET_CAPABILITY_NOT_METERED);
         withWifiUnderlying.setLinkUpstreamBandwidthKbps(20);
+        withWifiUnderlying.setUnderlyingNetworks(underlyingNetworksContainWifi);
 
         final NetworkCapabilities withWifiAndMobileUnderlying =
                 new NetworkCapabilities(withNoUnderlying);
@@ -7280,6 +7395,7 @@
         withWifiAndMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_ROAMING);
         withWifiAndMobileUnderlying.setLinkDownstreamBandwidthKbps(10);
         withWifiAndMobileUnderlying.setLinkUpstreamBandwidthKbps(20);
+        withWifiAndMobileUnderlying.setUnderlyingNetworks(underlyingNetworksContainMobileAndMobile);
 
         final NetworkCapabilities initialCapsNotMetered = new NetworkCapabilities(initialCaps);
         initialCapsNotMetered.addCapability(NET_CAPABILITY_NOT_METERED);
@@ -7287,40 +7403,61 @@
         NetworkCapabilities caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(new Network[]{}, initialCapsNotMetered, caps);
         assertEquals(withNoUnderlying, caps);
+        assertEquals(0, new ArrayList<>(caps.getUnderlyingNetworks()).size());
 
         caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(new Network[]{null}, initialCapsNotMetered, caps);
         assertEquals(withNoUnderlying, caps);
+        assertEquals(0, new ArrayList<>(caps.getUnderlyingNetworks()).size());
 
         caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(new Network[]{mobile}, initialCapsNotMetered, caps);
         assertEquals(withMobileUnderlying, caps);
+        assertEquals(1, new ArrayList<>(caps.getUnderlyingNetworks()).size());
+        assertEquals(mobile, new ArrayList<>(caps.getUnderlyingNetworks()).get(0));
 
+        caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(new Network[]{wifi}, initialCapsNotMetered, caps);
         assertEquals(withWifiUnderlying, caps);
+        assertEquals(1, new ArrayList<>(caps.getUnderlyingNetworks()).size());
+        assertEquals(wifi, new ArrayList<>(caps.getUnderlyingNetworks()).get(0));
 
         withWifiUnderlying.removeCapability(NET_CAPABILITY_NOT_METERED);
         caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(new Network[]{wifi}, initialCaps, caps);
         assertEquals(withWifiUnderlying, caps);
+        assertEquals(1, new ArrayList<>(caps.getUnderlyingNetworks()).size());
+        assertEquals(wifi, new ArrayList<>(caps.getUnderlyingNetworks()).get(0));
 
         caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(new Network[]{mobile, wifi}, initialCaps, caps);
         assertEquals(withWifiAndMobileUnderlying, caps);
+        assertEquals(2, new ArrayList<>(caps.getUnderlyingNetworks()).size());
+        assertEquals(mobile, new ArrayList<>(caps.getUnderlyingNetworks()).get(0));
+        assertEquals(wifi, new ArrayList<>(caps.getUnderlyingNetworks()).get(1));
 
         withWifiUnderlying.addCapability(NET_CAPABILITY_NOT_METERED);
         caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(new Network[]{null, mobile, null, wifi},
                 initialCapsNotMetered, caps);
         assertEquals(withWifiAndMobileUnderlying, caps);
+        assertEquals(2, new ArrayList<>(caps.getUnderlyingNetworks()).size());
+        assertEquals(mobile, new ArrayList<>(caps.getUnderlyingNetworks()).get(0));
+        assertEquals(wifi, new ArrayList<>(caps.getUnderlyingNetworks()).get(1));
 
         caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(new Network[]{null, mobile, null, wifi},
                 initialCapsNotMetered, caps);
         assertEquals(withWifiAndMobileUnderlying, caps);
+        assertEquals(2, new ArrayList<>(caps.getUnderlyingNetworks()).size());
+        assertEquals(mobile, new ArrayList<>(caps.getUnderlyingNetworks()).get(0));
+        assertEquals(wifi, new ArrayList<>(caps.getUnderlyingNetworks()).get(1));
 
+        caps = new NetworkCapabilities(initialCaps);
         mService.applyUnderlyingCapabilities(null, initialCapsNotMetered, caps);
         assertEquals(withWifiUnderlying, caps);
+        assertEquals(1, new ArrayList<>(caps.getUnderlyingNetworks()).size());
+        assertEquals(wifi, new ArrayList<>(caps.getUnderlyingNetworks()).get(0));
     }
 
     @Test
@@ -7329,51 +7466,78 @@
         final NetworkRequest request = new NetworkRequest.Builder()
                 .removeCapability(NET_CAPABILITY_NOT_VPN).build();
 
-        mCm.registerNetworkCallback(request, callback);
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mCm.registerNetworkCallback(request, callback);
 
-        // Bring up a VPN that specifies an underlying network that does not exist yet.
-        // Note: it's sort of meaningless for a VPN app to declare a network that doesn't exist yet,
-        // (and doing so is difficult without using reflection) but it's good to test that the code
-        // behaves approximately correctly.
-        mMockVpn.establishForMyUid(false, true, false);
-        assertUidRangesUpdatedForMyUid(true);
-        final Network wifiNetwork = new Network(mNetIdManager.peekNextNetId());
-        mMockVpn.setUnderlyingNetworks(new Network[]{wifiNetwork});
-        callback.expectAvailableCallbacksUnvalidated(mMockVpn);
-        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
-                .hasTransport(TRANSPORT_VPN));
-        assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
-                .hasTransport(TRANSPORT_WIFI));
+            // Bring up a VPN that specifies an underlying network that does not exist yet.
+            // Note: it's sort of meaningless for a VPN app to declare a network that doesn't exist
+            // yet, (and doing so is difficult without using reflection) but it's good to test that
+            // the code behaves approximately correctly.
+            mMockVpn.establishForMyUid(false, true, false);
+            callback.expectAvailableCallbacksUnvalidated(mMockVpn);
+            assertUidRangesUpdatedForMyUid(true);
+            final Network wifiNetwork = new Network(mNetIdManager.peekNextNetId());
+            mMockVpn.setUnderlyingNetworks(new Network[]{wifiNetwork});
+            // onCapabilitiesChanged() should be called because
+            // NetworkCapabilities#mUnderlyingNetworks is updated.
+            CallbackEntry ce = callback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED,
+                    mMockVpn);
+            final NetworkCapabilities vpnNc1 = ((CallbackEntry.CapabilitiesChanged) ce).getCaps();
+            // Since the wifi network hasn't brought up,
+            // ConnectivityService#applyUnderlyingCapabilities cannot find it. Update
+            // NetworkCapabilities#mUnderlyingNetworks to an empty array, and it will be updated to
+            // the correct underlying networks once the wifi network brings up. But this case
+            // shouldn't happen in reality since no one could get the network which hasn't brought
+            // up. For the empty array of underlying networks, it should be happened for 2 cases,
+            // the first one is that the VPN app declares an empty array for its underlying
+            // networks, the second one is that the underlying networks are torn down.
+            //
+            // It shouldn't be null since the null value means the underlying networks of this
+            // network should follow the default network.
+            final ArrayList<Network> underlyingNetwork = new ArrayList<>();
+            assertEquals(underlyingNetwork, vpnNc1.getUnderlyingNetworks());
+            // Since the wifi network isn't exist, applyUnderlyingCapabilities()
+            assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                    .hasTransport(TRANSPORT_VPN));
+            assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                    .hasTransport(TRANSPORT_WIFI));
 
-        // Make that underlying network connect, and expect to see its capabilities immediately
-        // reflected in the VPN's capabilities.
-        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
-        assertEquals(wifiNetwork, mWiFiNetworkAgent.getNetwork());
-        mWiFiNetworkAgent.connect(false);
-        // TODO: the callback for the VPN happens before any callbacks are called for the wifi
-        // network that has just connected. There appear to be two issues here:
-        // 1. The VPN code will accept an underlying network as soon as getNetworkCapabilities() for
-        //    it returns non-null (which happens very early, during handleRegisterNetworkAgent).
-        //    This is not correct because that that point the network is not connected and cannot
-        //    pass any traffic.
-        // 2. When a network connects, updateNetworkInfo propagates underlying network capabilities
-        //    before rematching networks.
-        // Given that this scenario can't really happen, this is probably fine for now.
-        callback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn);
-        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
-        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
-                .hasTransport(TRANSPORT_VPN));
-        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
-                .hasTransport(TRANSPORT_WIFI));
+            // Make that underlying network connect, and expect to see its capabilities immediately
+            // reflected in the VPN's capabilities.
+            mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+            assertEquals(wifiNetwork, mWiFiNetworkAgent.getNetwork());
+            mWiFiNetworkAgent.connect(false);
+            // TODO: the callback for the VPN happens before any callbacks are called for the wifi
+            // network that has just connected. There appear to be two issues here:
+            // 1. The VPN code will accept an underlying network as soon as getNetworkCapabilities()
+            //    for it returns non-null (which happens very early, during
+            //    handleRegisterNetworkAgent).
+            //    This is not correct because that that point the network is not connected and
+            //    cannot pass any traffic.
+            // 2. When a network connects, updateNetworkInfo propagates underlying network
+            //    capabilities before rematching networks.
+            // Given that this scenario can't really happen, this is probably fine for now.
+            ce = callback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn);
+            final NetworkCapabilities vpnNc2 = ((CallbackEntry.CapabilitiesChanged) ce).getCaps();
+            // The wifi network is brought up, NetworkCapabilities#mUnderlyingNetworks is updated to
+            // it.
+            underlyingNetwork.add(wifiNetwork);
+            assertEquals(underlyingNetwork, vpnNc2.getUnderlyingNetworks());
+            callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+            assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                    .hasTransport(TRANSPORT_VPN));
+            assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                    .hasTransport(TRANSPORT_WIFI));
 
-        // Disconnect the network, and expect to see the VPN capabilities change accordingly.
-        mWiFiNetworkAgent.disconnect();
-        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
-        callback.expectCapabilitiesThat(mMockVpn, (nc) ->
-                nc.getTransportTypes().length == 1 && nc.hasTransport(TRANSPORT_VPN));
+            // Disconnect the network, and expect to see the VPN capabilities change accordingly.
+            mWiFiNetworkAgent.disconnect();
+            callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+            callback.expectCapabilitiesThat(mMockVpn, (nc) ->
+                    nc.getTransportTypes().length == 1 && nc.hasTransport(TRANSPORT_VPN));
 
-        mMockVpn.disconnect();
-        mCm.unregisterNetworkCallback(callback);
+            mMockVpn.disconnect();
+            mCm.unregisterNetworkCallback(callback);
+        });
     }
 
     private void assertGetNetworkInfoOfGetActiveNetworkIsConnected(boolean expectedConnectivity) {
@@ -9108,18 +9272,20 @@
         mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
         mCellNetworkAgent.connect(true);
         waitForIdle();
-        verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
+        final ArrayTrackRecord<ReportedInterfaces>.ReadHead readHead =
+                mDeps.mReportedInterfaceHistory.newReadHead();
+        assertNotNull(readHead.poll(TIMEOUT_MS, ri -> ri.contentEquals(mServiceContext,
                 cellLp.getInterfaceName(),
-                new int[] { TRANSPORT_CELLULAR });
+                new int[] { TRANSPORT_CELLULAR })));
 
         final LinkProperties wifiLp = new LinkProperties();
         wifiLp.setInterfaceName("wifi0");
         mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
         mWiFiNetworkAgent.connect(true);
         waitForIdle();
-        verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
+        assertNotNull(readHead.poll(TIMEOUT_MS, ri -> ri.contentEquals(mServiceContext,
                 wifiLp.getInterfaceName(),
-                new int[] { TRANSPORT_WIFI });
+                new int[] { TRANSPORT_WIFI })));
 
         mCellNetworkAgent.disconnect();
         mWiFiNetworkAgent.disconnect();
@@ -9128,9 +9294,9 @@
         mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
         mCellNetworkAgent.connect(true);
         waitForIdle();
-        verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
+        assertNotNull(readHead.poll(TIMEOUT_MS, ri -> ri.contentEquals(mServiceContext,
                 cellLp.getInterfaceName(),
-                new int[] { TRANSPORT_CELLULAR });
+                new int[] { TRANSPORT_CELLULAR })));
         mCellNetworkAgent.disconnect();
     }
 
@@ -9213,9 +9379,11 @@
         assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default);
         verify(mMockDnsResolver, times(1)).createNetworkCache(eq(cellNetId));
         verify(mMockNetd, times(1)).networkAddInterface(cellNetId, MOBILE_IFNAME);
-        verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
+        final ArrayTrackRecord<ReportedInterfaces>.ReadHead readHead =
+                mDeps.mReportedInterfaceHistory.newReadHead();
+        assertNotNull(readHead.poll(TIMEOUT_MS, ri -> ri.contentEquals(mServiceContext,
                 cellLp.getInterfaceName(),
-                new int[] { TRANSPORT_CELLULAR });
+                new int[] { TRANSPORT_CELLULAR })));
 
         networkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
         verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId);
@@ -9234,8 +9402,8 @@
         // Make sure BatteryStats was not told about any v4- interfaces, as none should have
         // come online yet.
         waitForIdle();
-        verify(mDeps, never())
-                .reportNetworkInterfaceForTransports(eq(mServiceContext), startsWith("v4-"), any());
+        assertNull(readHead.poll(0 /* timeout */, ri -> mServiceContext.equals(ri.context)
+                && ri.iface != null && ri.iface.startsWith("v4-")));
 
         verifyNoMoreInteractions(mMockNetd);
         verifyNoMoreInteractions(mMockDnsResolver);
@@ -9287,9 +9455,9 @@
         assertTrue(CollectionUtils.contains(resolvrParams.servers, "8.8.8.8"));
 
         for (final LinkProperties stackedLp : stackedLpsAfterChange) {
-            verify(mDeps).reportNetworkInterfaceForTransports(
-                    mServiceContext, stackedLp.getInterfaceName(),
-                    new int[] { TRANSPORT_CELLULAR });
+            assertNotNull(readHead.poll(TIMEOUT_MS, ri -> ri.contentEquals(mServiceContext,
+                    stackedLp.getInterfaceName(),
+                    new int[] { TRANSPORT_CELLULAR })));
         }
         reset(mMockNetd);
         doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
@@ -9606,7 +9774,7 @@
 
     @Test
     public void testWith464XlatDisable() throws Exception {
-        doReturn(false).when(mDeps).getCellular464XlatEnabled();
+        mDeps.setCellular464XlatEnabled(false);
 
         final TestNetworkCallback callback = new TestNetworkCallback();
         final TestNetworkCallback defaultCallback = new TestNetworkCallback();
@@ -10464,7 +10632,7 @@
         final UnderlyingNetworkInfo underlyingNetworkInfo =
                 new UnderlyingNetworkInfo(vpnOwnerUid, VPN_IFNAME, new ArrayList<>());
         mMockVpn.setUnderlyingNetworkInfo(underlyingNetworkInfo);
-        doReturn(42).when(mDeps).getConnectionOwnerUid(anyInt(), any(), any());
+        mDeps.setConnectionOwnerUid(42);
     }
 
     private void setupConnectionOwnerUidAsVpnApp(int vpnOwnerUid, @VpnManager.VpnType int vpnType)
@@ -10697,6 +10865,14 @@
         return fakeNai(wifiNc, info);
     }
 
+    private NetworkAgentInfo fakeVpnNai(NetworkCapabilities nc) {
+        final NetworkCapabilities vpnNc = new NetworkCapabilities.Builder(nc)
+                .addTransportType(TRANSPORT_VPN).build();
+        final NetworkInfo info = new NetworkInfo(TYPE_VPN, 0 /* subtype */,
+                ConnectivityManager.getNetworkTypeName(TYPE_VPN), "" /* subtypeName */);
+        return fakeNai(vpnNc, info);
+    }
+
     private NetworkAgentInfo fakeNai(NetworkCapabilities nc, NetworkInfo networkInfo) {
         return new NetworkAgentInfo(null, new Network(NET_ID), networkInfo, new LinkProperties(),
                 nc, new NetworkScore.Builder().setLegacyInt(0).build(),
@@ -10831,6 +11007,36 @@
     }
 
     @Test
+    public void testUnderlyingNetworksWillBeSetInNetworkAgentInfoConstructor() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastT());
+        final Network network1 = new Network(100);
+        final Network network2 = new Network(101);
+        final List<Network> underlyingNetworks = new ArrayList<>();
+        final NetworkCapabilities ncWithEmptyUnderlyingNetworks = new NetworkCapabilities.Builder()
+                .setUnderlyingNetworks(underlyingNetworks)
+                .build();
+        final NetworkAgentInfo vpnNaiWithEmptyUnderlyingNetworks =
+                fakeVpnNai(ncWithEmptyUnderlyingNetworks);
+        assertEquals(underlyingNetworks,
+                Arrays.asList(vpnNaiWithEmptyUnderlyingNetworks.declaredUnderlyingNetworks));
+
+        underlyingNetworks.add(network1);
+        underlyingNetworks.add(network2);
+        final NetworkCapabilities ncWithUnderlyingNetworks = new NetworkCapabilities.Builder()
+                .setUnderlyingNetworks(underlyingNetworks)
+                .build();
+        final NetworkAgentInfo vpnNaiWithUnderlyingNetwokrs = fakeVpnNai(ncWithUnderlyingNetworks);
+        assertEquals(underlyingNetworks,
+                Arrays.asList(vpnNaiWithUnderlyingNetwokrs.declaredUnderlyingNetworks));
+
+        final NetworkCapabilities ncWithoutUnderlyingNetworks = new NetworkCapabilities.Builder()
+                .build();
+        final NetworkAgentInfo vpnNaiWithoutUnderlyingNetwokrs =
+                fakeVpnNai(ncWithoutUnderlyingNetworks);
+        assertNull(vpnNaiWithoutUnderlyingNetwokrs.declaredUnderlyingNetworks);
+    }
+
+    @Test
     public void testRegisterConnectivityDiagnosticsCallbackCallsOnConnectivityReport()
             throws Exception {
         // Set up the Network, which leads to a ConnectivityReport being cached for the network.
diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
index f358726..aa4c4e3 100644
--- a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -109,8 +109,8 @@
 
         mNai.linkProperties = new LinkProperties();
         mNai.linkProperties.setInterfaceName(BASE_IFACE);
-        mNai.networkInfo = new NetworkInfo(null);
-        mNai.networkInfo.setType(ConnectivityManager.TYPE_WIFI);
+        mNai.networkInfo = new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0 /* subtype */,
+                null /* typeName */, null /* subtypeName */);
         mNai.networkCapabilities = new NetworkCapabilities();
         markNetworkConnected();
         when(mNai.connService()).thenReturn(mConnectivity);
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index b706090..fd9aefa 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity;
 
+import static android.Manifest.permission.BIND_VPN_SERVICE;
 import static android.content.pm.UserInfo.FLAG_ADMIN;
 import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
 import static android.content.pm.UserInfo.FLAG_PRIMARY;
@@ -31,12 +32,14 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doCallRealMethod;
@@ -70,6 +73,7 @@
 import android.net.IpPrefix;
 import android.net.IpSecManager;
 import android.net.IpSecTunnelInterfaceResponse;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.LocalSocket;
 import android.net.Network;
@@ -86,6 +90,7 @@
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.INetworkManagementService;
+import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -102,6 +107,7 @@
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.server.IpSecService;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -118,6 +124,7 @@
 
 import java.io.BufferedWriter;
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.net.Inet4Address;
@@ -851,6 +858,81 @@
     }
 
     @Test
+    public void testStartOpAndFinishOpWillBeCalledWhenPlatformVpnIsOnAndOff() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastT());
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        verify(mAppOps).noteOpNoThrow(
+                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
+                eq(Process.myUid()),
+                eq(TEST_VPN_PKG),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+        verify(mAppOps).startOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+                eq(Process.myUid()),
+                eq(TEST_VPN_PKG),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+        // Add a small delay to make sure that startOp is only called once.
+        verify(mAppOps, after(100).times(1)).startOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+                eq(Process.myUid()),
+                eq(TEST_VPN_PKG),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+        // Check that the startOp is not called with OPSTR_ESTABLISH_VPN_SERVICE.
+        verify(mAppOps, never()).startOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
+                eq(Process.myUid()),
+                eq(TEST_VPN_PKG),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+        vpn.stopVpnProfile(TEST_VPN_PKG);
+        // Add a small delay to double confirm that startOp is only called once.
+        verify(mAppOps, after(100)).finishOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+                eq(Process.myUid()),
+                eq(TEST_VPN_PKG),
+                eq(null) /* attributionTag */);
+    }
+
+    @Test
+    public void testStartOpWithSeamlessHandover() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastT());
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN);
+        assertTrue(vpn.prepare(TEST_VPN_PKG, null, VpnManager.TYPE_VPN_SERVICE));
+        final VpnConfig config = new VpnConfig();
+        config.user = "VpnTest";
+        config.addresses.add(new LinkAddress("192.0.2.2/32"));
+        config.mtu = 1450;
+        final ResolveInfo resolveInfo = new ResolveInfo();
+        final ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.permission = BIND_VPN_SERVICE;
+        resolveInfo.serviceInfo = serviceInfo;
+        when(mPackageManager.resolveService(any(), anyInt())).thenReturn(resolveInfo);
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+        vpn.establish(config);
+        verify(mAppOps, times(1)).startOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
+                eq(Process.myUid()),
+                eq(TEST_VPN_PKG),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+        // Call establish() twice with the same config, it should match seamless handover case and
+        // startOp() shouldn't be called again.
+        vpn.establish(config);
+        verify(mAppOps, times(1)).startOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
+                eq(Process.myUid()),
+                eq(TEST_VPN_PKG),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+    }
+
+    @Test
     public void testSetPackageAuthorizationVpnService() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();
 
@@ -1197,6 +1279,32 @@
         public boolean isInterfacePresent(final Vpn vpn, final String iface) {
             return true;
         }
+
+        @Override
+        public ParcelFileDescriptor adoptFd(Vpn vpn, int mtu) {
+            return new ParcelFileDescriptor(new FileDescriptor());
+        }
+
+        @Override
+        public int jniCreate(Vpn vpn, int mtu) {
+            // Pick a random positive number as fd to return.
+            return 345;
+        }
+
+        @Override
+        public String jniGetName(Vpn vpn, int fd) {
+            return TEST_IFACE_NAME;
+        }
+
+        @Override
+        public int jniSetAddresses(Vpn vpn, String interfaze, String addresses) {
+            if (addresses == null) return 0;
+            // Return the number of addresses.
+            return addresses.split(" ").length;
+        }
+
+        @Override
+        public void setBlocking(FileDescriptor fd, boolean blocking) {}
     }
 
     /**
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index ab76460..4948e66 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -523,7 +523,7 @@
     public void testUidStatsAcrossNetworks() throws Exception {
         // pretend first mobile network comes online
         expectDefaultSettings();
-        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildMobile3gState(IMSI_1)};
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildMobileState(IMSI_1)};
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
 
@@ -554,7 +554,7 @@
         // disappearing, to verify we don't count backwards.
         incrementCurrentTime(HOUR_IN_MILLIS);
         expectDefaultSettings();
-        states = new NetworkStateSnapshot[] {buildMobile3gState(IMSI_2)};
+        states = new NetworkStateSnapshot[] {buildMobileState(IMSI_2)};
         expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L));
         expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
@@ -657,13 +657,16 @@
     @Test
     public void testMobileStatsByRatType() throws Exception {
         final NetworkTemplate template3g =
-                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS);
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS,
+                METERED_YES);
         final NetworkTemplate template4g =
-                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_LTE);
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_LTE,
+                METERED_YES);
         final NetworkTemplate template5g =
-                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_NR);
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_NR,
+                METERED_YES);
         final NetworkStateSnapshot[] states =
-                new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)};
+                new NetworkStateSnapshot[]{buildMobileState(IMSI_1)};
 
         // 3G network comes online.
         expectNetworkStatsSummary(buildEmptyStats());
@@ -730,6 +733,45 @@
     }
 
     @Test
+    public void testMobileStatsMeteredness() throws Exception {
+        // Create metered 5g template.
+        final NetworkTemplate templateMetered5g =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_NR,
+                METERED_YES);
+        // Create non-metered 5g template
+        final NetworkTemplate templateNonMetered5g =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_NR, METERED_NO);
+
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        // Pretend that 5g mobile network comes online
+        final NetworkStateSnapshot[] mobileStates =
+                new NetworkStateSnapshot[] {buildMobileState(IMSI_1), buildMobileState(TEST_IFACE2,
+                IMSI_1, true /* isTemporarilyNotMetered */, false /* isRoaming */)};
+        setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_NR);
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, mobileStates,
+                getActiveIface(mobileStates), new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic
+        // Note that all traffic from NetworkManagementService is tagged as METERED_NO, ROAMING_NO
+        // and DEFAULT_NETWORK_YES, because these three properties aren't tracked at that layer.
+        // They are layered on top by inspecting the iface properties.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0L)
+                .insertEntry(TEST_IFACE2, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 256, 3L, 128L, 5L, 0L));
+        forcePollAndWaitForIdle();
+
+        // Verify service recorded history.
+        assertUidTotal(templateMetered5g, UID_RED, 128L, 2L, 128L, 2L, 0);
+        assertUidTotal(templateNonMetered5g, UID_RED, 256, 3L, 128L, 5L, 0);
+    }
+
+    @Test
     public void testMobileStatsOemManaged() throws Exception {
         final NetworkTemplate templateOemPaid = new NetworkTemplate(MATCH_MOBILE_WILDCARD,
                 /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null,
@@ -1112,7 +1154,8 @@
         // pretend that network comes online
         expectDefaultSettings();
         NetworkStateSnapshot[] states =
-            new NetworkStateSnapshot[] {buildMobile3gState(IMSI_1, true /* isRoaming */)};
+            new NetworkStateSnapshot[] {buildMobileState(TEST_IFACE, IMSI_1,
+            false /* isTemporarilyNotMetered */, true /* isRoaming */)};
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
 
@@ -1151,7 +1194,7 @@
         // pretend first mobile network comes online
         expectDefaultSettings();
         final NetworkStateSnapshot[] states =
-                new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)};
+                new NetworkStateSnapshot[]{buildMobileState(IMSI_1)};
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
 
@@ -1478,13 +1521,15 @@
         // Build 3G template, type unknown template to get stats while network type is unknown
         // and type all template to get the sum of all network type stats.
         final NetworkTemplate template3g =
-                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS);
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS,
+                METERED_YES);
         final NetworkTemplate templateUnknown =
-                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                METERED_YES);
         final NetworkTemplate templateAll =
-                buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL);
+                buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL, METERED_YES);
         final NetworkStateSnapshot[] states =
-                new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)};
+                new NetworkStateSnapshot[]{buildMobileState(IMSI_1)};
 
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
@@ -1561,7 +1606,7 @@
         // Pretend mobile network comes online, but wifi is the default network.
         expectDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
-                buildWifiState(true /*isMetered*/, TEST_IFACE2), buildMobile3gState(IMSI_1)};
+                buildWifiState(true /*isMetered*/, TEST_IFACE2), buildMobileState(IMSI_1)};
         expectNetworkStatsUidDetail(buildEmptyStats());
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
@@ -1580,7 +1625,7 @@
 
         // Verify mobile summary is not changed by the operation count.
         final NetworkTemplate templateMobile =
-                buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL);
+                buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL, METERED_YES);
         final NetworkStats statsMobile = mSession.getSummaryForAllUid(
                 templateMobile, Long.MIN_VALUE, Long.MAX_VALUE, true);
         assertValues(statsMobile, IFACE_ALL, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
@@ -1655,6 +1700,8 @@
         return states[0].getLinkProperties().getInterfaceName();
     }
 
+    // TODO: These expect* methods are used to have NetworkStatsService returns the given stats
+    //       instead of expecting anything. Therefore, these methods should be renamed properly.
     private void expectNetworkStatsSummary(NetworkStats summary) throws Exception {
         expectNetworkStatsSummaryDev(summary.clone());
         expectNetworkStatsSummaryXt(summary.clone());
@@ -1744,15 +1791,21 @@
         return new NetworkStateSnapshot(WIFI_NETWORK, capabilities, prop, subscriberId, TYPE_WIFI);
     }
 
-    private static NetworkStateSnapshot buildMobile3gState(String subscriberId) {
-        return buildMobile3gState(subscriberId, false /* isRoaming */);
+    private static NetworkStateSnapshot buildMobileState(String subscriberId) {
+        return buildMobileState(TEST_IFACE, subscriberId, false /* isTemporarilyNotMetered */,
+                false /* isRoaming */);
     }
 
-    private static NetworkStateSnapshot buildMobile3gState(String subscriberId, boolean isRoaming) {
+    private static NetworkStateSnapshot buildMobileState(String iface, String subscriberId,
+            boolean isTemporarilyNotMetered, boolean isRoaming) {
         final LinkProperties prop = new LinkProperties();
-        prop.setInterfaceName(TEST_IFACE);
+        prop.setInterfaceName(iface);
         final NetworkCapabilities capabilities = new NetworkCapabilities();
-        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false);
+
+        if (isTemporarilyNotMetered) {
+            capabilities.addCapability(
+                    NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+        }
         capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming);
         capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
         return new NetworkStateSnapshot(