Merge "Reduce BROADCAST_TIMEOUT_MS."
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1b2c0ed..95f854b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -3,6 +3,10 @@
     {
       "name": "ConnectivityCoverageTests"
     },
+    {
+      // In addition to ConnectivityCoverageTests, runs non-connectivity-module tests
+      "name": "FrameworksNetTests"
+    },
     // Run in addition to mainline-presubmit as mainline-presubmit is not
     // supported in every branch.
     // CtsNetTestCasesLatestSdk uses stable API shims, so does not exercise
@@ -19,6 +23,12 @@
       ]
     },
     {
+      "name": "bpf_existence_test"
+    },
+    {
+      "name": "connectivity_native_test"
+    },
+    {
       "name": "netd_updatable_unit_test"
     },
     {
@@ -41,10 +51,6 @@
     {
       "name": "TetheringPrivilegedTests"
     },
-    // TODO: move to presubmit when known green.
-    {
-      "name": "bpf_existence_test"
-    },
     {
       "name": "netd_updatable_unit_test",
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
@@ -76,6 +82,12 @@
       ]
     },
     {
+      "name": "bpf_existence_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    {
+      "name": "connectivity_native_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    {
       "name": "netd_updatable_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     },
     {
@@ -111,7 +123,10 @@
       ]
     },
     // TODO: move to mainline-presubmit when known green.
-    // Test with connectivity/tethering module only, to catch integration issues with older versions of other modules
+    // Test with connectivity/tethering module only, to catch integration issues with older versions of other modules.
+    // "new tethering + old NetworkStack" is not a configuration that should really exist in the field, but
+    // there is no strong guarantee, and it is required by MTS testing for module qualification, where modules
+    // are tested independently.
     {
       "name": "CtsNetTestCasesLatestSdk[com.google.android.tethering.apex]",
       "options": [
@@ -122,13 +137,6 @@
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
-    },
-    {
-      "name": "TetheringCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
-    },
-    // TODO: move to mainline-presubmit when known green.
-    {
-      "name": "bpf_existence_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "auto-postsubmit": [
@@ -160,9 +168,6 @@
     },
     {
       "path": "packages/modules/CaptivePortalLogin"
-    },
-    {
-      "path": "packages/modules/Connectivity/Tethering"
     }
   ]
 }
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 46fd50f..29f6e12 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -47,9 +47,11 @@
         "net-utils-device-common-bpf",
         "net-utils-device-common-netlink",
         "netd-client",
+        "tetheringstatsprotos",
     ],
     libs: [
         "framework-connectivity",
+        "framework-connectivity-t.stubs.module_lib",
         "framework-statsd.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi",
@@ -210,3 +212,14 @@
     bootclasspath_fragments: ["com.android.tethering-bootclasspath-fragment"],
     systemserverclasspath_fragments: ["com.android.tethering-systemserverclasspath-fragment"],
 }
+
+java_library_static {
+    name: "tetheringstatsprotos",
+    proto: {type: "lite"},
+    srcs: [
+        "src/com/android/networkstack/tethering/metrics/stats.proto",
+    ],
+    static_libs: ["tetheringprotos"],
+    apex_available: ["com.android.tethering"],
+    min_sdk_version: "30",
+}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 1a4ba9d..bd8fe7c 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -38,10 +38,11 @@
 
 apex {
     name: "com.android.tethering",
-    defaults: ["ConnectivityApexDefaults"],
+    defaults: [
+        "ConnectivityApexDefaults",
+        "r-launched-apex-module",
+    ],
     compile_multilib: "both",
-    updatable: true,
-    min_sdk_version: "30",
     bootclasspath_fragments: [
         "com.android.tethering-bootclasspath-fragment",
     ],
@@ -57,7 +58,10 @@
             native_shared_libs: ["libnetd_updatable"],
         },
         both: {
-            jni_libs: ["libframework-connectivity-jni"],
+            jni_libs: [
+                "libframework-connectivity-jni",
+                "libframework-connectivity-tiramisu-jni"
+            ],
         },
     },
     binaries: [
@@ -65,9 +69,10 @@
     ],
     canned_fs_config: "canned_fs_config",
     bpfs: [
+        "block.o",
         "clatd.o_mainline",
-        "netd.o_mainline",
         "dscp_policy.o",
+        "netd.o_mainline",
         "offload.o",
         "test.o",
     ],
@@ -85,6 +90,8 @@
     compressible: true,
 
     androidManifest: "AndroidManifest.xml",
+
+    compat_configs: ["connectivity-platform-compat-config"],
 }
 
 apex_key {
@@ -103,7 +110,7 @@
     name: "com.android.tethering-bootclasspath-fragment",
     contents: [
         "framework-connectivity",
-        "framework-connectivity-tiramisu",
+        "framework-connectivity-t",
         "framework-tethering",
     ],
     apex_available: ["com.android.tethering"],
@@ -127,12 +134,41 @@
     hidden_api: {
         max_target_r_low_priority: [
             "hiddenapi/hiddenapi-max-target-r-loprio.txt",
-	],
+        ],
         max_target_o_low_priority: [
             "hiddenapi/hiddenapi-max-target-o-low-priority.txt",
             "hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt",
-	],
-        unsupported: ["hiddenapi/hiddenapi-unsupported.txt"],
+        ],
+        unsupported: [
+            "hiddenapi/hiddenapi-unsupported.txt",
+            "hiddenapi/hiddenapi-unsupported-tiramisu.txt",
+        ],
+
+        // The following packages contain classes from other modules on the
+        // bootclasspath. That means that the hidden API flags for this module
+        // has to explicitly list every single class this module provides in
+        // that package to differentiate them from the classes provided by other
+        // modules. That can include private classes that are not part of the
+        // API.
+        split_packages: [
+            "android.app.usage",
+            "android.net",
+            "android.net.netstats",
+            "android.net.util",
+        ],
+
+        // The following packages and all their subpackages currently only
+        // contain classes from this bootclasspath_fragment. Listing a package
+        // here won't prevent other bootclasspath modules from adding classes in
+        // any of those packages but it will prevent them from adding those
+        // classes into an API surface, e.g. public, system, etc.. Doing so will
+        // result in a build failure due to inconsistent flags.
+        package_prefixes: [
+            "android.net.apf",
+            "android.net.connectivity",
+            "android.net.netstats.provider",
+            "android.net.nsd",
+        ],
     },
 }
 
diff --git a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt
index c1d87bb..ce0d69c 100644
--- a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt
+++ b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt
@@ -1,3 +1,123 @@
+Landroid/app/usage/NetworkStats$Bucket;->convertDefaultNetworkStatus(I)I
+Landroid/app/usage/NetworkStats$Bucket;->convertMetered(I)I
+Landroid/app/usage/NetworkStats$Bucket;->convertRoaming(I)I
+Landroid/app/usage/NetworkStats$Bucket;->convertSet(I)I
+Landroid/app/usage/NetworkStats$Bucket;->convertState(I)I
+Landroid/app/usage/NetworkStats$Bucket;->convertTag(I)I
+Landroid/app/usage/NetworkStats$Bucket;->convertUid(I)I
+Landroid/app/usage/NetworkStats$Bucket;->mBeginTimeStamp:J
+Landroid/app/usage/NetworkStats$Bucket;->mDefaultNetworkStatus:I
+Landroid/app/usage/NetworkStats$Bucket;->mEndTimeStamp:J
+Landroid/app/usage/NetworkStats$Bucket;->mMetered:I
+Landroid/app/usage/NetworkStats$Bucket;->mRoaming:I
+Landroid/app/usage/NetworkStats$Bucket;->mRxBytes:J
+Landroid/app/usage/NetworkStats$Bucket;->mRxPackets:J
+Landroid/app/usage/NetworkStats$Bucket;->mState:I
+Landroid/app/usage/NetworkStats$Bucket;->mTag:I
+Landroid/app/usage/NetworkStats$Bucket;->mTxBytes:J
+Landroid/app/usage/NetworkStats$Bucket;->mTxPackets:J
+Landroid/app/usage/NetworkStats$Bucket;->mUid:I
+Landroid/app/usage/NetworkStats;-><init>(Landroid/content/Context;Landroid/net/NetworkTemplate;IJJLandroid/net/INetworkStatsService;)V
+Landroid/app/usage/NetworkStats;->fillBucketFromSummaryEntry(Landroid/app/usage/NetworkStats$Bucket;)V
+Landroid/app/usage/NetworkStats;->getDeviceSummaryForNetwork()Landroid/app/usage/NetworkStats$Bucket;
+Landroid/app/usage/NetworkStats;->getNextHistoryBucket(Landroid/app/usage/NetworkStats$Bucket;)Z
+Landroid/app/usage/NetworkStats;->getNextSummaryBucket(Landroid/app/usage/NetworkStats$Bucket;)Z
+Landroid/app/usage/NetworkStats;->getSummaryAggregate()Landroid/app/usage/NetworkStats$Bucket;
+Landroid/app/usage/NetworkStats;->getUid()I
+Landroid/app/usage/NetworkStats;->hasNextUid()Z
+Landroid/app/usage/NetworkStats;->isUidEnumeration()Z
+Landroid/app/usage/NetworkStats;->mCloseGuard:Ldalvik/system/CloseGuard;
+Landroid/app/usage/NetworkStats;->mEndTimeStamp:J
+Landroid/app/usage/NetworkStats;->mEnumerationIndex:I
+Landroid/app/usage/NetworkStats;->mHistory:Landroid/net/NetworkStatsHistory;
+Landroid/app/usage/NetworkStats;->mRecycledHistoryEntry:Landroid/net/NetworkStatsHistory$Entry;
+Landroid/app/usage/NetworkStats;->mRecycledSummaryEntry:Landroid/net/NetworkStats$Entry;
+Landroid/app/usage/NetworkStats;->mSession:Landroid/net/INetworkStatsSession;
+Landroid/app/usage/NetworkStats;->mStartTimeStamp:J
+Landroid/app/usage/NetworkStats;->mState:I
+Landroid/app/usage/NetworkStats;->mSummary:Landroid/net/NetworkStats;
+Landroid/app/usage/NetworkStats;->mTag:I
+Landroid/app/usage/NetworkStats;->mTemplate:Landroid/net/NetworkTemplate;
+Landroid/app/usage/NetworkStats;->mUidOrUidIndex:I
+Landroid/app/usage/NetworkStats;->mUids:[I
+Landroid/app/usage/NetworkStats;->setSingleUidTagState(III)V
+Landroid/app/usage/NetworkStats;->startHistoryEnumeration(III)V
+Landroid/app/usage/NetworkStats;->startSummaryEnumeration()V
+Landroid/app/usage/NetworkStats;->startUserUidEnumeration()V
+Landroid/app/usage/NetworkStats;->stepHistory()V
+Landroid/app/usage/NetworkStats;->stepUid()V
+Landroid/app/usage/NetworkStats;->TAG:Ljava/lang/String;
+Landroid/app/usage/NetworkStatsManager$CallbackHandler;-><init>(Landroid/os/Looper;ILjava/lang/String;Landroid/app/usage/NetworkStatsManager$UsageCallback;)V
+Landroid/app/usage/NetworkStatsManager$CallbackHandler;->getObject(Landroid/os/Message;Ljava/lang/String;)Ljava/lang/Object;
+Landroid/app/usage/NetworkStatsManager$CallbackHandler;->mCallback:Landroid/app/usage/NetworkStatsManager$UsageCallback;
+Landroid/app/usage/NetworkStatsManager$CallbackHandler;->mNetworkType:I
+Landroid/app/usage/NetworkStatsManager$CallbackHandler;->mSubscriberId:Ljava/lang/String;
+Landroid/app/usage/NetworkStatsManager$UsageCallback;->request:Landroid/net/DataUsageRequest;
+Landroid/app/usage/NetworkStatsManager;-><init>(Landroid/content/Context;Landroid/net/INetworkStatsService;)V
+Landroid/app/usage/NetworkStatsManager;->CALLBACK_LIMIT_REACHED:I
+Landroid/app/usage/NetworkStatsManager;->CALLBACK_RELEASED:I
+Landroid/app/usage/NetworkStatsManager;->createTemplate(ILjava/lang/String;)Landroid/net/NetworkTemplate;
+Landroid/app/usage/NetworkStatsManager;->DBG:Z
+Landroid/app/usage/NetworkStatsManager;->FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN:I
+Landroid/app/usage/NetworkStatsManager;->FLAG_POLL_FORCE:I
+Landroid/app/usage/NetworkStatsManager;->FLAG_POLL_ON_OPEN:I
+Landroid/app/usage/NetworkStatsManager;->mContext:Landroid/content/Context;
+Landroid/app/usage/NetworkStatsManager;->mFlags:I
+Landroid/app/usage/NetworkStatsManager;->MIN_THRESHOLD_BYTES:J
+Landroid/app/usage/NetworkStatsManager;->mService:Landroid/net/INetworkStatsService;
+Landroid/app/usage/NetworkStatsManager;->querySummaryForDevice(Landroid/net/NetworkTemplate;JJ)Landroid/app/usage/NetworkStats$Bucket;
+Landroid/app/usage/NetworkStatsManager;->registerUsageCallback(Landroid/net/NetworkTemplate;IJLandroid/app/usage/NetworkStatsManager$UsageCallback;Landroid/os/Handler;)V
+Landroid/app/usage/NetworkStatsManager;->setAugmentWithSubscriptionPlan(Z)V
+Landroid/app/usage/NetworkStatsManager;->setPollOnOpen(Z)V
+Landroid/app/usage/NetworkStatsManager;->TAG:Ljava/lang/String;
+Landroid/net/DataUsageRequest;-><init>(ILandroid/net/NetworkTemplate;J)V
+Landroid/net/DataUsageRequest;->CREATOR:Landroid/os/Parcelable$Creator;
+Landroid/net/DataUsageRequest;->PARCELABLE_KEY:Ljava/lang/String;
+Landroid/net/DataUsageRequest;->requestId:I
+Landroid/net/DataUsageRequest;->REQUEST_ID_UNSET:I
+Landroid/net/DataUsageRequest;->template:Landroid/net/NetworkTemplate;
+Landroid/net/DataUsageRequest;->thresholdInBytes:J
+Landroid/net/EthernetManager;-><init>(Landroid/content/Context;Landroid/net/IEthernetManager;)V
+Landroid/net/EthernetManager;->mContext:Landroid/content/Context;
+Landroid/net/EthernetManager;->mHandler:Landroid/os/Handler;
+Landroid/net/EthernetManager;->mListeners:Ljava/util/ArrayList;
+Landroid/net/EthernetManager;->mService:Landroid/net/IEthernetManager;
+Landroid/net/EthernetManager;->mServiceListener:Landroid/net/IEthernetServiceListener$Stub;
+Landroid/net/EthernetManager;->MSG_AVAILABILITY_CHANGED:I
+Landroid/net/EthernetManager;->TAG:Ljava/lang/String;
+Landroid/net/IEthernetManager$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
+Landroid/net/IEthernetManager$Stub$Proxy;->addListener(Landroid/net/IEthernetServiceListener;)V
+Landroid/net/IEthernetManager$Stub$Proxy;->getAvailableInterfaces()[Ljava/lang/String;
+Landroid/net/IEthernetManager$Stub$Proxy;->getConfiguration(Ljava/lang/String;)Landroid/net/IpConfiguration;
+Landroid/net/IEthernetManager$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
+Landroid/net/IEthernetManager$Stub$Proxy;->isAvailable(Ljava/lang/String;)Z
+Landroid/net/IEthernetManager$Stub$Proxy;->mRemote:Landroid/os/IBinder;
+Landroid/net/IEthernetManager$Stub$Proxy;->removeListener(Landroid/net/IEthernetServiceListener;)V
+Landroid/net/IEthernetManager$Stub$Proxy;->setConfiguration(Ljava/lang/String;Landroid/net/IpConfiguration;)V
+Landroid/net/IEthernetManager$Stub;-><init>()V
+Landroid/net/IEthernetManager$Stub;->asInterface(Landroid/os/IBinder;)Landroid/net/IEthernetManager;
+Landroid/net/IEthernetManager$Stub;->DESCRIPTOR:Ljava/lang/String;
+Landroid/net/IEthernetManager$Stub;->TRANSACTION_addListener:I
+Landroid/net/IEthernetManager$Stub;->TRANSACTION_getAvailableInterfaces:I
+Landroid/net/IEthernetManager$Stub;->TRANSACTION_getConfiguration:I
+Landroid/net/IEthernetManager$Stub;->TRANSACTION_isAvailable:I
+Landroid/net/IEthernetManager$Stub;->TRANSACTION_removeListener:I
+Landroid/net/IEthernetManager$Stub;->TRANSACTION_setConfiguration:I
+Landroid/net/IEthernetManager;->addListener(Landroid/net/IEthernetServiceListener;)V
+Landroid/net/IEthernetManager;->getAvailableInterfaces()[Ljava/lang/String;
+Landroid/net/IEthernetManager;->getConfiguration(Ljava/lang/String;)Landroid/net/IpConfiguration;
+Landroid/net/IEthernetManager;->isAvailable(Ljava/lang/String;)Z
+Landroid/net/IEthernetManager;->removeListener(Landroid/net/IEthernetServiceListener;)V
+Landroid/net/IEthernetManager;->setConfiguration(Ljava/lang/String;Landroid/net/IpConfiguration;)V
+Landroid/net/IEthernetServiceListener$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
+Landroid/net/IEthernetServiceListener$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
+Landroid/net/IEthernetServiceListener$Stub$Proxy;->mRemote:Landroid/os/IBinder;
+Landroid/net/IEthernetServiceListener$Stub$Proxy;->onAvailabilityChanged(Ljava/lang/String;Z)V
+Landroid/net/IEthernetServiceListener$Stub;-><init>()V
+Landroid/net/IEthernetServiceListener$Stub;->asInterface(Landroid/os/IBinder;)Landroid/net/IEthernetServiceListener;
+Landroid/net/IEthernetServiceListener$Stub;->DESCRIPTOR:Ljava/lang/String;
+Landroid/net/IEthernetServiceListener$Stub;->TRANSACTION_onAvailabilityChanged:I
+Landroid/net/IEthernetServiceListener;->onAvailabilityChanged(Ljava/lang/String;Z)V
 Landroid/net/IIpSecService$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
 Landroid/net/IIpSecService$Stub$Proxy;->addAddressToTunnelInterface(ILandroid/net/LinkAddress;Ljava/lang/String;)V
 Landroid/net/IIpSecService$Stub$Proxy;->allocateSecurityParameterIndex(Ljava/lang/String;ILandroid/os/IBinder;)Landroid/net/IpSecSpiResponse;
@@ -43,6 +163,68 @@
 Landroid/net/IIpSecService;->releaseSecurityParameterIndex(I)V
 Landroid/net/IIpSecService;->removeAddressFromTunnelInterface(ILandroid/net/LinkAddress;Ljava/lang/String;)V
 Landroid/net/IIpSecService;->removeTransportModeTransforms(Landroid/os/ParcelFileDescriptor;)V
+Landroid/net/INetworkStatsService$Stub$Proxy;->forceUpdate()V
+Landroid/net/INetworkStatsService$Stub$Proxy;->forceUpdateIfaces([Landroid/net/Network;)V
+Landroid/net/INetworkStatsService$Stub$Proxy;->getDataLayerSnapshotForUid(I)Landroid/net/NetworkStats;
+Landroid/net/INetworkStatsService$Stub$Proxy;->getDetailedUidStats([Ljava/lang/String;)Landroid/net/NetworkStats;
+Landroid/net/INetworkStatsService$Stub$Proxy;->getIfaceStats(Ljava/lang/String;I)J
+Landroid/net/INetworkStatsService$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
+Landroid/net/INetworkStatsService$Stub$Proxy;->getTotalStats(I)J
+Landroid/net/INetworkStatsService$Stub$Proxy;->getUidStats(II)J
+Landroid/net/INetworkStatsService$Stub$Proxy;->incrementOperationCount(III)V
+Landroid/net/INetworkStatsService$Stub$Proxy;->mRemote:Landroid/os/IBinder;
+Landroid/net/INetworkStatsService$Stub$Proxy;->openSession()Landroid/net/INetworkStatsSession;
+Landroid/net/INetworkStatsService$Stub$Proxy;->openSessionForUsageStats(ILjava/lang/String;)Landroid/net/INetworkStatsSession;
+Landroid/net/INetworkStatsService$Stub$Proxy;->registerUsageCallback(Ljava/lang/String;Landroid/net/DataUsageRequest;Landroid/os/Messenger;Landroid/os/IBinder;)Landroid/net/DataUsageRequest;
+Landroid/net/INetworkStatsService$Stub$Proxy;->unregisterUsageRequest(Landroid/net/DataUsageRequest;)V
+Landroid/net/INetworkStatsService$Stub;-><init>()V
+Landroid/net/INetworkStatsService$Stub;->DESCRIPTOR:Ljava/lang/String;
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_forceUpdate:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_forceUpdateIfaces:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_getDataLayerSnapshotForUid:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_getDetailedUidStats:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_getIfaceStats:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_getMobileIfaces:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_getTotalStats:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_getUidStats:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_incrementOperationCount:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_openSession:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_openSessionForUsageStats:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_registerUsageCallback:I
+Landroid/net/INetworkStatsService$Stub;->TRANSACTION_unregisterUsageRequest:I
+Landroid/net/INetworkStatsService;->forceUpdateIfaces([Landroid/net/Network;)V
+Landroid/net/INetworkStatsService;->getDetailedUidStats([Ljava/lang/String;)Landroid/net/NetworkStats;
+Landroid/net/INetworkStatsService;->getIfaceStats(Ljava/lang/String;I)J
+Landroid/net/INetworkStatsService;->getTotalStats(I)J
+Landroid/net/INetworkStatsService;->getUidStats(II)J
+Landroid/net/INetworkStatsService;->incrementOperationCount(III)V
+Landroid/net/INetworkStatsService;->registerUsageCallback(Ljava/lang/String;Landroid/net/DataUsageRequest;Landroid/os/Messenger;Landroid/os/IBinder;)Landroid/net/DataUsageRequest;
+Landroid/net/INetworkStatsService;->unregisterUsageRequest(Landroid/net/DataUsageRequest;)V
+Landroid/net/INetworkStatsSession$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
+Landroid/net/INetworkStatsSession$Stub$Proxy;->close()V
+Landroid/net/INetworkStatsSession$Stub$Proxy;->getDeviceSummaryForNetwork(Landroid/net/NetworkTemplate;JJ)Landroid/net/NetworkStats;
+Landroid/net/INetworkStatsSession$Stub$Proxy;->getHistoryForNetwork(Landroid/net/NetworkTemplate;I)Landroid/net/NetworkStatsHistory;
+Landroid/net/INetworkStatsSession$Stub$Proxy;->getHistoryForUid(Landroid/net/NetworkTemplate;IIII)Landroid/net/NetworkStatsHistory;
+Landroid/net/INetworkStatsSession$Stub$Proxy;->getHistoryIntervalForUid(Landroid/net/NetworkTemplate;IIIIJJ)Landroid/net/NetworkStatsHistory;
+Landroid/net/INetworkStatsSession$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
+Landroid/net/INetworkStatsSession$Stub$Proxy;->getRelevantUids()[I
+Landroid/net/INetworkStatsSession$Stub$Proxy;->getSummaryForAllUid(Landroid/net/NetworkTemplate;JJZ)Landroid/net/NetworkStats;
+Landroid/net/INetworkStatsSession$Stub$Proxy;->getSummaryForNetwork(Landroid/net/NetworkTemplate;JJ)Landroid/net/NetworkStats;
+Landroid/net/INetworkStatsSession$Stub$Proxy;->mRemote:Landroid/os/IBinder;
+Landroid/net/INetworkStatsSession$Stub;-><init>()V
+Landroid/net/INetworkStatsSession$Stub;->asInterface(Landroid/os/IBinder;)Landroid/net/INetworkStatsSession;
+Landroid/net/INetworkStatsSession$Stub;->DESCRIPTOR:Ljava/lang/String;
+Landroid/net/INetworkStatsSession$Stub;->TRANSACTION_close:I
+Landroid/net/INetworkStatsSession$Stub;->TRANSACTION_getDeviceSummaryForNetwork:I
+Landroid/net/INetworkStatsSession$Stub;->TRANSACTION_getHistoryForNetwork:I
+Landroid/net/INetworkStatsSession$Stub;->TRANSACTION_getHistoryForUid:I
+Landroid/net/INetworkStatsSession$Stub;->TRANSACTION_getHistoryIntervalForUid:I
+Landroid/net/INetworkStatsSession$Stub;->TRANSACTION_getRelevantUids:I
+Landroid/net/INetworkStatsSession$Stub;->TRANSACTION_getSummaryForAllUid:I
+Landroid/net/INetworkStatsSession$Stub;->TRANSACTION_getSummaryForNetwork:I
+Landroid/net/INetworkStatsSession;->getDeviceSummaryForNetwork(Landroid/net/NetworkTemplate;JJ)Landroid/net/NetworkStats;
+Landroid/net/INetworkStatsSession;->getHistoryIntervalForUid(Landroid/net/NetworkTemplate;IIIIJJ)Landroid/net/NetworkStatsHistory;
+Landroid/net/INetworkStatsSession;->getRelevantUids()[I
 Landroid/net/IpSecAlgorithm;->checkValidOrThrow(Ljava/lang/String;II)V
 Landroid/net/IpSecAlgorithm;->CRYPT_NULL:Ljava/lang/String;
 Landroid/net/IpSecAlgorithm;->equals(Landroid/net/IpSecAlgorithm;Landroid/net/IpSecAlgorithm;)Z
@@ -298,3 +480,38 @@
 Landroid/net/nsd/NsdServiceInfo;->mTxtRecord:Landroid/util/ArrayMap;
 Landroid/net/nsd/NsdServiceInfo;->setTxtRecords(Ljava/lang/String;)V
 Landroid/net/nsd/NsdServiceInfo;->TAG:Ljava/lang/String;
+Landroid/net/TrafficStats;->addIfSupported(J)J
+Landroid/net/TrafficStats;->closeQuietly(Landroid/net/INetworkStatsSession;)V
+Landroid/net/TrafficStats;->GB_IN_BYTES:J
+Landroid/net/TrafficStats;->getDataLayerSnapshotForUid(Landroid/content/Context;)Landroid/net/NetworkStats;
+Landroid/net/TrafficStats;->getRxPackets(Ljava/lang/String;)J
+Landroid/net/TrafficStats;->getTxPackets(Ljava/lang/String;)J
+Landroid/net/TrafficStats;->KB_IN_BYTES:J
+Landroid/net/TrafficStats;->LOOPBACK_IFACE:Ljava/lang/String;
+Landroid/net/TrafficStats;->MB_IN_BYTES:J
+Landroid/net/TrafficStats;->PB_IN_BYTES:J
+Landroid/net/TrafficStats;->sActiveProfilingStart:Landroid/net/NetworkStats;
+Landroid/net/TrafficStats;->sProfilingLock:Ljava/lang/Object;
+Landroid/net/TrafficStats;->sStatsService:Landroid/net/INetworkStatsService;
+Landroid/net/TrafficStats;->startDataProfiling(Landroid/content/Context;)V
+Landroid/net/TrafficStats;->stopDataProfiling(Landroid/content/Context;)Landroid/net/NetworkStats;
+Landroid/net/TrafficStats;->TAG_SYSTEM_APP:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_BACKUP:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_DHCP:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_DOWNLOAD:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_GPS:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_MEDIA:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_NEIGHBOR:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_NTP:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_PAC:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_PROBE:I
+Landroid/net/TrafficStats;->TAG_SYSTEM_RESTORE:I
+Landroid/net/TrafficStats;->TB_IN_BYTES:J
+Landroid/net/TrafficStats;->TYPE_RX_BYTES:I
+Landroid/net/TrafficStats;->TYPE_RX_PACKETS:I
+Landroid/net/TrafficStats;->TYPE_TCP_RX_PACKETS:I
+Landroid/net/TrafficStats;->TYPE_TCP_TX_PACKETS:I
+Landroid/net/TrafficStats;->TYPE_TX_BYTES:I
+Landroid/net/TrafficStats;->TYPE_TX_PACKETS:I
+Landroid/net/TrafficStats;->UID_REMOVED:I
+Landroid/net/TrafficStats;->UID_TETHERING:I
diff --git a/Tethering/apex/hiddenapi/hiddenapi-unsupported-tiramisu.txt b/Tethering/apex/hiddenapi/hiddenapi-unsupported-tiramisu.txt
new file mode 100644
index 0000000..a6257e3
--- /dev/null
+++ b/Tethering/apex/hiddenapi/hiddenapi-unsupported-tiramisu.txt
@@ -0,0 +1,3 @@
+Landroid/net/INetworkStatsService$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
+Landroid/net/INetworkStatsService$Stub$Proxy;->getMobileIfaces()[Ljava/lang/String;
+Landroid/net/INetworkStatsService$Stub;->asInterface(Landroid/os/IBinder;)Landroid/net/INetworkStatsService;
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 22d2c5d..b865a8e 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -30,9 +30,9 @@
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
-import com.android.networkstack.tethering.TetherStatsValue;
 
 /**
  * Bpf coordinator class for API shims.
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 5afb862..0683e5e 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -33,6 +33,8 @@
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.BpfUtils;
@@ -42,8 +44,6 @@
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherStatsKey;
-import com.android.networkstack.tethering.TetherStatsValue;
 import com.android.networkstack.tethering.TetherUpstream6Key;
 
 import java.io.FileDescriptor;
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 915e210..69cbab5 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -25,9 +25,9 @@
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
-import com.android.networkstack.tethering.TetherStatsValue;
 
 /**
  * Bpf coordinator class for API shims.
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 9ca3f14..25489ff 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -21,6 +21,7 @@
     name: "framework-tethering",
     defaults: ["framework-module-defaults"],
     impl_library_visibility: [
+        "//frameworks/base/packages/Tethering:__subpackages__",
         "//packages/modules/Connectivity/Tethering:__subpackages__",
 
         // Using for test only
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index 5de4b97..40eed3f 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -12,3 +12,5 @@
 
 # Classes from net-utils-device-common
 rule com.android.net.module.util.Struct* com.android.networkstack.tethering.util.Struct@1
+
+rule com.google.protobuf.** com.android.networkstack.tethering.protobuf@1
\ No newline at end of file
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 6735317..7b5ae0d 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -19,3 +19,9 @@
 -keepclassmembers class android.net.ip.IpServer {
     static final int CMD_*;
 }
+
+# The lite proto runtime uses reflection to access fields based on the names in
+# the schema, keep all the fields.
+-keepclassmembers class * extends com.android.networkstack.tethering.protobuf.MessageLite {
+    <fields>;
+}
\ No newline at end of file
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
index 0412a49..bfec5bc 100644
--- a/Tethering/res/values/config.xml
+++ b/Tethering/res/values/config.xml
@@ -78,6 +78,12 @@
     <!-- Use legacy wifi p2p dedicated address instead of randomize address. -->
     <bool translatable="false" name="config_tether_enable_legacy_wifi_p2p_dedicated_ip">false</bool>
 
+    <!-- Use lease subnet prefix length to reserve the range outside of subnet prefix length.
+         This configuration only valid if its value larger than dhcp server address prefix length
+         and config_tether_enable_legacy_wifi_p2p_dedicated_ip is true.
+    -->
+    <integer translatable="false" name="config_p2p_leases_subnet_prefix_length">0</integer>
+
     <!-- Dhcp range (min, max) to use for tethering purposes -->
     <string-array translatable="false" name="config_tether_dhcp_range">
     </string-array>
diff --git a/Tethering/res/values/overlayable.xml b/Tethering/res/values/overlayable.xml
index 91fbd7d..7bd905c 100644
--- a/Tethering/res/values/overlayable.xml
+++ b/Tethering/res/values/overlayable.xml
@@ -32,6 +32,7 @@
             <item type="bool" name="config_tether_enable_bpf_offload"/>
             <item type="bool" name="config_tether_enable_legacy_dhcp_server"/>
             <item type="bool" name="config_tether_enable_legacy_wifi_p2p_dedicated_ip"/>
+            <item type="integer" name="config_p2p_leases_subnet_prefix_length"/>
             <item type="integer" name="config_tether_offload_poll_interval"/>
             <item type="array" name="config_tether_upstream_types"/>
             <item type="bool" name="config_tether_upstream_automatic"/>
diff --git a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
index aaaec17..8d58945 100644
--- a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
+++ b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
@@ -185,6 +185,16 @@
         return this;
     }
 
+    /** Set leases subnet prefix length. If the value is smaller than server address prefix length,
+     * this configuration will be ignored.
+     *
+     * <p>If not set, the default value is zero.
+     */
+    public DhcpServingParamsParcelExt setLeasesSubnetPrefixLength(int prefixLength) {
+        this.leasesSubnetPrefixLength = prefixLength;
+        return this;
+    }
+
     private static int[] toIntArray(@NonNull Collection<Inet4Address> addrs) {
         int[] res = new int[addrs.size()];
         int i = 0;
diff --git a/Tethering/src/android/net/ip/DadProxy.java b/Tethering/src/android/net/ip/DadProxy.java
index e2976b7..36ecfe3 100644
--- a/Tethering/src/android/net/ip/DadProxy.java
+++ b/Tethering/src/android/net/ip/DadProxy.java
@@ -16,11 +16,12 @@
 
 package android.net.ip;
 
-import android.net.util.InterfaceParams;
 import android.os.Handler;
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.net.module.util.InterfaceParams;
+
 /**
  * Basic Duplicate address detection proxy.
  *
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 2bb19db..c718f4c 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -46,7 +46,6 @@
 import android.net.dhcp.IDhcpServer;
 import android.net.ip.IpNeighborMonitor.NeighborEvent;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.Looper;
@@ -63,11 +62,13 @@
 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.InterfaceParams;
 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;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
+import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 
@@ -240,6 +241,7 @@
     private final LinkProperties mLinkProperties;
     private final boolean mUsingLegacyDhcp;
     private final boolean mUsingBpfOffload;
+    private final int mP2pLeasesSubnetPrefixLength;
 
     private final Dependencies mDeps;
 
@@ -285,8 +287,8 @@
     public IpServer(
             String ifaceName, Looper looper, int interfaceType, SharedLog log,
             INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
-            boolean usingLegacyDhcp, boolean usingBpfOffload,
-            PrivateAddressCoordinator addressCoordinator, Dependencies deps) {
+            TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
+            Dependencies deps) {
         super(ifaceName, looper);
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
@@ -296,8 +298,9 @@
         mIfaceName = ifaceName;
         mInterfaceType = interfaceType;
         mLinkProperties = new LinkProperties();
-        mUsingLegacyDhcp = usingLegacyDhcp;
-        mUsingBpfOffload = usingBpfOffload;
+        mUsingLegacyDhcp = config.useLegacyDhcpServer();
+        mUsingBpfOffload = config.isBpfOffloadEnabled();
+        mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
         mPrivateAddressCoordinator = addressCoordinator;
         mDeps = deps;
         resetLinkProperties();
@@ -526,6 +529,9 @@
             @Nullable Inet4Address clientAddr) {
         final boolean changePrefixOnDecline =
                 (mInterfaceType == TetheringManager.TETHERING_NCM && clientAddr == null);
+        final int subnetPrefixLength = mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
+                ? mP2pLeasesSubnetPrefixLength : 0 /* default value */;
+
         return new DhcpServingParamsParcelExt()
             .setDefaultRouters(defaultRouter)
             .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS)
@@ -533,7 +539,8 @@
             .setServerAddr(serverAddr)
             .setMetered(true)
             .setSingleClientAddr(clientAddr)
-            .setChangePrefixOnDecline(changePrefixOnDecline);
+            .setChangePrefixOnDecline(changePrefixOnDecline)
+            .setLeasesSubnetPrefixLength(subnetPrefixLength);
             // TODO: also advertise link MTU
     }
 
diff --git a/Tethering/src/android/net/ip/NeighborPacketForwarder.java b/Tethering/src/android/net/ip/NeighborPacketForwarder.java
index 27e59a1..723bd63 100644
--- a/Tethering/src/android/net/ip/NeighborPacketForwarder.java
+++ b/Tethering/src/android/net/ip/NeighborPacketForwarder.java
@@ -24,13 +24,13 @@
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
 
-import android.net.util.InterfaceParams;
 import android.net.util.SocketUtils;
 import android.os.Handler;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Log;
 
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.PacketReader;
 import com.android.networkstack.tethering.util.TetheringUtils;
 
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 74f9369..c452e55 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -37,7 +37,6 @@
 import android.net.LinkAddress;
 import android.net.MacAddress;
 import android.net.TrafficStats;
-import android.net.util.InterfaceParams;
 import android.net.util.SocketUtils;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -45,6 +44,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.LlaOption;
 import com.android.net.module.util.structs.MtuOption;
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 6550de2..bba0a40 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -44,7 +44,6 @@
 import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
 import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.SystemClock;
@@ -64,10 +63,13 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
-import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
@@ -1177,22 +1179,13 @@
         }
     }
 
-    /**
-     * Simple struct that only contains a u32. Must be public because Struct needs access to it.
-     * TODO: make this a public inner class of Struct so anyone can use it as, e.g., Struct.U32?
-     */
-    public static class U32Struct extends Struct {
-        @Struct.Field(order = 0, type = Struct.Type.U32)
-        public long val;
-    }
-
     private void dumpCounters(@NonNull IndentingPrintWriter pw) {
         if (!mDeps.isAtLeastS()) {
             pw.println("No counter support");
             return;
         }
-        try (BpfMap<U32Struct, U32Struct> map = new BpfMap<>(TETHER_ERROR_MAP_PATH,
-                BpfMap.BPF_F_RDONLY, U32Struct.class, U32Struct.class)) {
+        try (BpfMap<U32, U32> map = new BpfMap<>(TETHER_ERROR_MAP_PATH, BpfMap.BPF_F_RDONLY,
+                U32.class, U32.class)) {
 
             map.forEach((k, v) -> {
                 String counterName;
@@ -1496,7 +1489,8 @@
     }
 
     @NonNull
-    private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
+    @VisibleForTesting
+    static byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
         final byte[] addr4 = ia4.getAddress();
         final byte[] addr6 = new byte[16];
         addr6[10] = (byte) 0xff;
@@ -2041,5 +2035,13 @@
         return mBpfConntrackEventConsumer;
     }
 
+    // Return tethering client information. This is used for testing only.
+    @NonNull
+    @VisibleForTesting
+    final HashMap<IpServer, HashMap<Inet4Address, ClientInfo>>
+            getTetherClientsForTesting() {
+        return mTetherClients;
+    }
+
     private static native String[] getBpfCounterNames();
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
index 77efb51..3d2dfaa 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
@@ -20,10 +20,9 @@
 
 import static com.android.networkstack.tethering.util.TetheringUtils.getTetheringJniLibraryName;
 
-import android.net.util.InterfaceParams;
-
 import androidx.annotation.NonNull;
 
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.TcUtils;
 
 import java.io.IOException;
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index 4ca36df..844efde 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -16,6 +16,7 @@
 
 package com.android.networkstack.tethering;
 
+import static android.content.pm.PackageManager.GET_ACTIVITIES;
 import static android.net.TetheringConstants.EXTRA_ADD_TETHER_TYPE;
 import static android.net.TetheringConstants.EXTRA_PROVISION_CALLBACK;
 import static android.net.TetheringConstants.EXTRA_RUN_PROVISION;
@@ -32,6 +33,9 @@
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
 
+import static com.android.networkstack.apishim.ConstantsShim.ACTION_TETHER_UNSUPPORTED_CARRIER_UI;
+import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -39,6 +43,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.net.util.SharedLog;
 import android.os.Bundle;
 import android.os.Handler;
@@ -52,6 +57,7 @@
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 
 import java.io.PrintWriter;
 import java.util.BitSet;
@@ -74,6 +80,13 @@
     protected static final String ACTION_PROVISIONING_ALARM =
             "com.android.networkstack.tethering.PROVISIONING_RECHECK_ALARM";
 
+    // Indicate tether provisioning is not required by carrier.
+    private static final int TETHERING_PROVISIONING_REQUIRED = 1000;
+    // Indicate tether provisioning is required by carrier.
+    private static final int TETHERING_PROVISIONING_NOT_REQUIRED = 1001;
+    // Indicate tethering is not supported by carrier.
+    private static final int TETHERING_PROVISIONING_CARRIER_UNSUPPORT = 1002;
+
     private final ComponentName mSilentProvisioningService;
     private static final int MS_PER_HOUR = 60 * 60 * 1000;
     private static final int DUMP_TIMEOUT = 10_000;
@@ -96,7 +109,7 @@
     private boolean mLastCellularUpstreamPermitted = true;
     private boolean mUsingCellularAsUpstream = false;
     private boolean mNeedReRunProvisioningUi = false;
-    private OnUiEntitlementFailedListener mListener;
+    private OnTetherProvisioningFailedListener mListener;
     private TetheringConfigurationFetcher mFetcher;
 
     public EntitlementManager(Context ctx, Handler h, SharedLog log,
@@ -115,18 +128,20 @@
                 mContext.getResources().getString(R.string.config_wifi_tether_enable));
     }
 
-    public void setOnUiEntitlementFailedListener(final OnUiEntitlementFailedListener listener) {
+    public void setOnTetherProvisioningFailedListener(
+            final OnTetherProvisioningFailedListener listener) {
         mListener = listener;
     }
 
     /** Callback fired when UI entitlement failed. */
-    public interface OnUiEntitlementFailedListener {
+    public interface OnTetherProvisioningFailedListener {
         /**
          * Ui entitlement check fails in |downstream|.
          *
          * @param downstream tethering type from TetheringManager.TETHERING_{@code *}.
+         * @param reason Failed reason.
          */
-        void onUiEntitlementFailed(int downstream);
+        void onTetherProvisioningFailed(int downstream, String reason);
     }
 
     public void setTetheringConfigurationFetcher(final TetheringConfigurationFetcher fetcher) {
@@ -153,6 +168,9 @@
     }
 
     private boolean isCellularUpstreamPermitted(final TetheringConfiguration config) {
+        // If #getTetherProvisioningCondition return TETHERING_PROVISIONING_CARRIER_UNSUPPORT,
+        // that means cellular upstream is not supported and entitlement check result is empty
+        // because entitlement check should not be run.
         if (!isTetherProvisioningRequired(config)) return true;
 
         // If provisioning is required and EntitlementManager doesn't know any downstreams, cellular
@@ -199,11 +217,7 @@
         // If upstream is not cellular, provisioning app would not be launched
         // till upstream change to cellular.
         if (mUsingCellularAsUpstream) {
-            if (showProvisioningUi) {
-                runUiTetherProvisioning(downstreamType, config);
-            } else {
-                runSilentTetherProvisioning(downstreamType, config);
-            }
+            runTetheringProvisioning(showProvisioningUi, downstreamType, config);
             mNeedReRunProvisioningUi = false;
         } else {
             mNeedReRunProvisioningUi |= showProvisioningUi;
@@ -262,18 +276,51 @@
         // the change and get the new correct value.
         for (int downstream = mCurrentDownstreams.nextSetBit(0); downstream >= 0;
                 downstream = mCurrentDownstreams.nextSetBit(downstream + 1)) {
+            // If tethering provisioning is required but entitlement check result is empty,
+            // this means tethering may need to run entitlement check or carrier network
+            // is not supported.
             if (mCurrentEntitlementResults.indexOfKey(downstream) < 0) {
-                if (mNeedReRunProvisioningUi) {
-                    mNeedReRunProvisioningUi = false;
-                    runUiTetherProvisioning(downstream, config);
-                } else {
-                    runSilentTetherProvisioning(downstream, config);
-                }
+                runTetheringProvisioning(mNeedReRunProvisioningUi, downstream, config);
+                mNeedReRunProvisioningUi = false;
             }
         }
     }
 
     /**
+     * Tether provisioning has these conditions to control provisioning behavior.
+     *  1st priority : Uses system property to disable any provisioning behavior.
+     *  2nd priority : Uses {@code CarrierConfigManager#KEY_CARRIER_SUPPORTS_TETHERING_BOOL} to
+     *                 decide current carrier support cellular upstream tethering or not.
+     *                 If value is true, it means check follow up condition to know whether
+     *                 provisioning is required.
+     *                 If value is false, it means tethering could not use cellular as upstream.
+     *  3rd priority : Uses {@code CarrierConfigManager#KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL} to
+     *                 decide current carrier require the provisioning.
+     *  4th priority : Checks whether provisioning is required from RRO configuration.
+     *
+     * @param config
+     * @return integer {@see #TETHERING_PROVISIONING_NOT_REQUIRED,
+     *                 #TETHERING_PROVISIONING_REQUIRED,
+     *                 #TETHERING_PROVISIONING_CARRIER_UNSUPPORT}
+     */
+    private int getTetherProvisioningCondition(final TetheringConfiguration config) {
+        if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)) {
+            return TETHERING_PROVISIONING_NOT_REQUIRED;
+        }
+        // TODO: Find a way to avoid get carrier config twice.
+        if (carrierConfigAffirmsCarrierNotSupport(config)) {
+            // To block tethering, behave as if running provisioning check and failed.
+            return TETHERING_PROVISIONING_CARRIER_UNSUPPORT;
+        }
+
+        if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+            return TETHERING_PROVISIONING_NOT_REQUIRED;
+        }
+        return (config.provisioningApp.length == 2)
+                ? TETHERING_PROVISIONING_REQUIRED : TETHERING_PROVISIONING_NOT_REQUIRED;
+    }
+
+    /**
      * Check if the device requires a provisioning check in order to enable tethering.
      *
      * @param config an object that encapsulates the various tethering configuration elements.
@@ -281,14 +328,26 @@
      */
     @VisibleForTesting
     protected boolean isTetherProvisioningRequired(final TetheringConfiguration config) {
-        if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
-                || config.provisioningApp.length == 0) {
+        return getTetherProvisioningCondition(config) != TETHERING_PROVISIONING_NOT_REQUIRED;
+    }
+
+    /**
+     * Confirms the need of tethering provisioning but no entitlement package exists.
+     */
+    public boolean isProvisioningNeededButUnavailable() {
+        final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+        return getTetherProvisioningCondition(config) == TETHERING_PROVISIONING_REQUIRED
+                && !doesEntitlementPackageExist(config);
+    }
+
+    private boolean doesEntitlementPackageExist(final TetheringConfiguration config) {
+        final PackageManager pm = mContext.getPackageManager();
+        try {
+            pm.getPackageInfo(config.provisioningApp[0], GET_ACTIVITIES);
+        } catch (PackageManager.NameNotFoundException e) {
             return false;
         }
-        if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
-            return false;
-        }
-        return (config.provisioningApp.length == 2);
+        return true;
     }
 
     /**
@@ -310,9 +369,7 @@
         mEntitlementCacheValue.clear();
         mCurrentEntitlementResults.clear();
 
-        // TODO: refine provisioning check to isTetherProvisioningRequired() ??
-        if (!config.hasMobileHotspotProvisionApp()
-                || carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+        if (!isTetherProvisioningRequired(config)) {
             evaluateCellularPermission(config);
             return;
         }
@@ -327,8 +384,8 @@
      * @param config an object that encapsulates the various tethering configuration elements.
      * */
     public PersistableBundle getCarrierConfig(final TetheringConfiguration config) {
-        final CarrierConfigManager configManager = (CarrierConfigManager) mContext
-                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        final CarrierConfigManager configManager = mContext
+                .getSystemService(CarrierConfigManager.class);
         if (configManager == null) return null;
 
         final PersistableBundle carrierConfig = configManager.getConfigForSubId(
@@ -346,6 +403,7 @@
     //
     // TODO: find a better way to express this, or alter the checking process
     // entirely so that this is more intuitive.
+    // TODO: Find a way to avoid using getCarrierConfig everytime.
     private boolean carrierConfigAffirmsEntitlementCheckNotRequired(
             final TetheringConfiguration config) {
         // Check carrier config for entitlement checks
@@ -358,16 +416,29 @@
         return !isEntitlementCheckRequired;
     }
 
+    private boolean carrierConfigAffirmsCarrierNotSupport(final TetheringConfiguration config) {
+        if (!SdkLevel.isAtLeastT()) {
+            return false;
+        }
+        // Check carrier config for entitlement checks
+        final PersistableBundle carrierConfig = getCarrierConfig(config);
+        if (carrierConfig == null) return false;
+
+        // A CarrierConfigManager was found and it has a config.
+        final boolean mIsCarrierSupport = carrierConfig.getBoolean(
+                KEY_CARRIER_SUPPORTS_TETHERING_BOOL, true);
+        return !mIsCarrierSupport;
+    }
+
     /**
      * Run no UI tethering provisioning check.
      * @param type tethering type from TetheringManager.TETHERING_{@code *}
      * @param subId default data subscription ID.
      */
     @VisibleForTesting
-    protected Intent runSilentTetherProvisioning(int type, final TetheringConfiguration config) {
+    protected Intent runSilentTetherProvisioning(
+            int type, final TetheringConfiguration config, ResultReceiver receiver) {
         if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
-        // For silent provisioning, settings would stop tethering when entitlement fail.
-        ResultReceiver receiver = buildProxyReceiver(type, false/* notifyFail */, null);
 
         Intent intent = new Intent();
         intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
@@ -383,11 +454,6 @@
         return intent;
     }
 
-    private void runUiTetherProvisioning(int type, final TetheringConfiguration config) {
-        ResultReceiver receiver = buildProxyReceiver(type, true/* notifyFail */, null);
-        runUiTetherProvisioning(type, config, receiver);
-    }
-
     /**
      * Run the UI-enabled tethering provisioning check.
      * @param type tethering type from TetheringManager.TETHERING_{@code *}
@@ -411,6 +477,35 @@
         return intent;
     }
 
+    private void runTetheringProvisioning(
+            boolean showProvisioningUi, int downstreamType, final TetheringConfiguration config) {
+        if (carrierConfigAffirmsCarrierNotSupport(config)) {
+            mListener.onTetherProvisioningFailed(downstreamType, "Carrier does not support.");
+            if (showProvisioningUi) {
+                showCarrierUnsupportedDialog();
+            }
+            return;
+        }
+
+        ResultReceiver receiver =
+                buildProxyReceiver(downstreamType, showProvisioningUi/* notifyFail */, null);
+        if (showProvisioningUi) {
+            runUiTetherProvisioning(downstreamType, config, receiver);
+        } else {
+            runSilentTetherProvisioning(downstreamType, config, receiver);
+        }
+    }
+
+    private void showCarrierUnsupportedDialog() {
+        // This is only used when carrierConfigAffirmsCarrierNotSupport() is true.
+        if (!SdkLevel.isAtLeastT()) {
+            return;
+        }
+        Intent intent = new Intent(ACTION_TETHER_UNSUPPORTED_CARRIER_UI);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+    }
+
     @VisibleForTesting
     PendingIntent createRecheckAlarmIntent() {
         final Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
@@ -576,7 +671,8 @@
                 int updatedCacheValue = updateEntitlementCacheValue(type, resultCode);
                 addDownstreamMapping(type, updatedCacheValue);
                 if (updatedCacheValue == TETHER_ERROR_PROVISIONING_FAILED && notifyFail) {
-                    mListener.onUiEntitlementFailed(type);
+                    mListener.onTetherProvisioningFailed(
+                            type, "Tethering provisioning failed.");
                 }
                 if (receiver != null) receiver.send(updatedCacheValue, null);
             }
@@ -632,9 +728,14 @@
         }
 
         final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
-        if (!isTetherProvisioningRequired(config)) {
-            receiver.send(TETHER_ERROR_NO_ERROR, null);
-            return;
+
+        switch (getTetherProvisioningCondition(config)) {
+            case TETHERING_PROVISIONING_NOT_REQUIRED:
+                receiver.send(TETHER_ERROR_NO_ERROR, null);
+                return;
+            case TETHERING_PROVISIONING_CARRIER_UNSUPPORT:
+                receiver.send(TETHER_ERROR_PROVISIONING_FAILED, null);
+                return;
         }
 
         final int cacheValue = mEntitlementCacheValue.get(
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index c1e7127..cc2422f 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -90,11 +90,8 @@
         mCachedAddresses.put(TETHERING_BLUETOOTH, new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS));
         mCachedAddresses.put(TETHERING_WIFI_P2P, new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS));
 
-        mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16")));
-        if (config.isSelectAllPrefixRangeEnabled()) {
-            mTetheringPrefixes.add(new IpPrefix("172.16.0.0/12"));
-            mTetheringPrefixes.add(new IpPrefix("10.0.0.0/8"));
-        }
+        mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"),
+            new IpPrefix("172.16.0.0/12"), new IpPrefix("10.0.0.0/8")));
     }
 
     /**
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
deleted file mode 100644
index 5442480..0000000
--- a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.networkstack.tethering;
-
-import com.android.net.module.util.Struct;
-import com.android.net.module.util.Struct.Field;
-import com.android.net.module.util.Struct.Type;
-
-/** The key of BpfMap which is used for tethering stats. */
-public class TetherStatsKey extends Struct {
-    @Field(order = 0, type = Type.U32)
-    public final long ifindex;  // upstream interface index
-
-    public TetherStatsKey(final long ifindex) {
-        this.ifindex = ifindex;
-    }
-
-    // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) return true;
-
-        if (!(obj instanceof TetherStatsKey)) return false;
-
-        final TetherStatsKey that = (TetherStatsKey) obj;
-
-        return ifindex == that.ifindex;
-    }
-
-    @Override
-    public int hashCode() {
-        return Long.hashCode(ifindex);
-    }
-
-    @Override
-    public String toString() {
-        return String.format("ifindex: %d", ifindex);
-    }
-}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
deleted file mode 100644
index 844d2e8..0000000
--- a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.networkstack.tethering;
-
-import com.android.net.module.util.Struct;
-import com.android.net.module.util.Struct.Field;
-import com.android.net.module.util.Struct.Type;
-
-/** The key of BpfMap which is used for tethering stats. */
-public class TetherStatsValue extends Struct {
-    // Use the signed long variable to store the uint64 stats from stats BPF map.
-    // U63 is enough for each data element even at 5Gbps for ~468 years.
-    // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468.
-    @Field(order = 0, type = Type.U63)
-    public final long rxPackets;
-    @Field(order = 1, type = Type.U63)
-    public final long rxBytes;
-    @Field(order = 2, type = Type.U63)
-    public final long rxErrors;
-    @Field(order = 3, type = Type.U63)
-    public final long txPackets;
-    @Field(order = 4, type = Type.U63)
-    public final long txBytes;
-    @Field(order = 5, type = Type.U63)
-    public final long txErrors;
-
-    public TetherStatsValue(final long rxPackets, final long rxBytes, final long rxErrors,
-            final long txPackets, final long txBytes, final long txErrors) {
-        this.rxPackets = rxPackets;
-        this.rxBytes = rxBytes;
-        this.rxErrors = rxErrors;
-        this.txPackets = txPackets;
-        this.txBytes = txBytes;
-        this.txErrors = txErrors;
-    }
-
-    // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) return true;
-
-        if (!(obj instanceof TetherStatsValue)) return false;
-
-        final TetherStatsValue that = (TetherStatsValue) obj;
-
-        return rxPackets == that.rxPackets
-                && rxBytes == that.rxBytes
-                && rxErrors == that.rxErrors
-                && txPackets == that.txPackets
-                && txBytes == that.txBytes
-                && txErrors == that.txErrors;
-    }
-
-    @Override
-    public int hashCode() {
-        return Long.hashCode(rxPackets) ^ Long.hashCode(rxBytes) ^ Long.hashCode(rxErrors)
-                ^ Long.hashCode(txPackets) ^ Long.hashCode(txBytes) ^ Long.hashCode(txErrors);
-    }
-
-    @Override
-    public String toString() {
-        return String.format("rxPackets: %s, rxBytes: %s, rxErrors: %s, txPackets: %s, "
-                + "txBytes: %s, txErrors: %s", rxPackets, rxBytes, rxErrors, txPackets,
-                txBytes, txErrors);
-    }
-}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 301a682..0b607bd 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -18,7 +18,6 @@
 
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.NETWORK_STACK;
-import static android.content.pm.PackageManager.GET_ACTIVITIES;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.hardware.usb.UsbManager.USB_CONFIGURED;
 import static android.hardware.usb.UsbManager.USB_CONNECTED;
@@ -319,8 +318,8 @@
         mEntitlementMgr = mDeps.getEntitlementManager(mContext, mHandler, mLog,
                 () -> mTetherMainSM.sendMessage(
                 TetherMainSM.EVENT_UPSTREAM_PERMISSION_CHANGED));
-        mEntitlementMgr.setOnUiEntitlementFailedListener((int downstream) -> {
-            mLog.log("OBSERVED UiEnitlementFailed");
+        mEntitlementMgr.setOnTetherProvisioningFailedListener((downstream, reason) -> {
+            mLog.log("OBSERVED OnTetherProvisioningFailed : " + reason);
             stopTethering(downstream);
         });
         mEntitlementMgr.setTetheringConfigurationFetcher(() -> {
@@ -995,30 +994,11 @@
         return tetherState.lastError;
     }
 
-    private boolean isProvisioningNeededButUnavailable() {
-        return isTetherProvisioningRequired() && !doesEntitlementPackageExist();
-    }
-
     boolean isTetherProvisioningRequired() {
         final TetheringConfiguration cfg = mConfig;
         return mEntitlementMgr.isTetherProvisioningRequired(cfg);
     }
 
-    private boolean doesEntitlementPackageExist() {
-        // provisioningApp must contain package and class name.
-        if (mConfig.provisioningApp.length != 2) {
-            return false;
-        }
-
-        final PackageManager pm = mContext.getPackageManager();
-        try {
-            pm.getPackageInfo(mConfig.provisioningApp[0], GET_ACTIVITIES);
-        } catch (PackageManager.NameNotFoundException e) {
-            return false;
-        }
-        return true;
-    }
-
     private int getRequestedState(int type) {
         final TetheringRequestParcel request = mActiveTetheringRequests.get(type);
 
@@ -1533,16 +1513,28 @@
         return mConfig;
     }
 
-    boolean hasTetherableConfiguration() {
-        final TetheringConfiguration cfg = mConfig;
-        final boolean hasDownstreamConfiguration =
-                (cfg.tetherableUsbRegexs.length != 0)
-                || (cfg.tetherableWifiRegexs.length != 0)
-                || (cfg.tetherableBluetoothRegexs.length != 0);
-        final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty()
-                || cfg.chooseUpstreamAutomatically;
+    boolean hasAnySupportedDownstream() {
+        if ((mConfig.tetherableUsbRegexs.length != 0)
+                || (mConfig.tetherableWifiRegexs.length != 0)
+                || (mConfig.tetherableBluetoothRegexs.length != 0)) {
+            return true;
+        }
 
-        return hasDownstreamConfiguration && hasUpstreamConfiguration;
+        // Before T, isTetheringSupported would return true if wifi, usb and bluetooth tethering are
+        // disabled (whole tethering settings would be hidden). This means tethering would also not
+        // support wifi p2p, ethernet tethering and mirrorlink. This is wrong but probably there are
+        // some devices in the field rely on this to disable tethering entirely.
+        if (!SdkLevel.isAtLeastT()) return false;
+
+        return (mConfig.tetherableWifiP2pRegexs.length != 0)
+                || (mConfig.tetherableNcmRegexs.length != 0)
+                || isEthernetSupported();
+    }
+
+    // TODO: using EtherentManager new API to check whether ethernet is supported when the API is
+    // ready to use.
+    private boolean isEthernetSupported() {
+        return mContext.getSystemService(Context.ETHERNET_SERVICE) != null;
     }
 
     void setUsbTethering(boolean enable, IIntResultListener listener) {
@@ -1745,7 +1737,7 @@
 
             // TODO: Randomize DHCPv4 ranges, especially in hotspot mode.
             // Legacy DHCP server is disabled if passed an empty ranges array
-            final String[] dhcpRanges = cfg.enableLegacyDhcpServer
+            final String[] dhcpRanges = cfg.useLegacyDhcpServer()
                     ? cfg.legacyDhcpRanges : new String[0];
             try {
                 NetdUtils.tetherStart(mNetd, true /** usingLegacyDnsProxy */, dhcpRanges);
@@ -2463,8 +2455,8 @@
         final boolean tetherEnabledInSettings = tetherSupported
                 && !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING);
 
-        return tetherEnabledInSettings && hasTetherableConfiguration()
-                && !isProvisioningNeededButUnavailable();
+        return tetherEnabledInSettings && hasAnySupportedDownstream()
+                && !mEntitlementMgr.isProvisioningNeededButUnavailable();
     }
 
     private void dumpBpf(IndentingPrintWriter pw) {
@@ -2722,8 +2714,7 @@
         mLog.i("adding IpServer for: " + iface);
         final TetherState tetherState = new TetherState(
                 new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
-                             makeControlCallback(), mConfig.enableLegacyDhcpServer,
-                             mConfig.isBpfOffloadEnabled(), mPrivateAddressCoordinator,
+                             makeControlCallback(), mConfig, mPrivateAddressCoordinator,
                              mDeps.getIpServerDependencies()), isNcm);
         mTetherStates.put(iface, tetherState);
         tetherState.ipServer.start();
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index dd0d567..f9f3ed9 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -99,13 +99,6 @@
             "use_legacy_wifi_p2p_dedicated_ip";
 
     /**
-     * Flag use to enable select all prefix ranges feature.
-     * TODO: Remove this flag if there are no problems after M-2020-12 rolls out.
-     */
-    public static final String TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES =
-            "tether_enable_select_all_prefix_ranges";
-
-    /**
      * Experiment flag to force choosing upstreams automatically.
      *
      * This setting is intended to help force-enable the feature on OEM devices that disabled it
@@ -143,7 +136,6 @@
     public final Collection<Integer> preferredUpstreamIfaceTypes;
     public final String[] legacyDhcpRanges;
     public final String[] defaultIPv4DNS;
-    public final boolean enableLegacyDhcpServer;
 
     public final String[] provisioningApp;
     public final String provisioningAppNoUi;
@@ -152,12 +144,13 @@
 
     public final int activeDataSubId;
 
+    private final boolean mEnableLegacyDhcpServer;
     private final int mOffloadPollInterval;
     // TODO: Add to TetheringConfigurationParcel if required.
     private final boolean mEnableBpfOffload;
     private final boolean mEnableWifiP2pDedicatedIp;
+    private final int mP2pLeasesSubnetPrefixLength;
 
-    private final boolean mEnableSelectAllPrefixRange;
     private final int mUsbTetheringFunction;
     protected final ContentResolver mContentResolver;
 
@@ -203,7 +196,7 @@
         legacyDhcpRanges = getLegacyDhcpRanges(res);
         defaultIPv4DNS = copy(DEFAULT_IPV4_DNS);
         mEnableBpfOffload = getEnableBpfOffload(res);
-        enableLegacyDhcpServer = getEnableLegacyDhcpServer(res);
+        mEnableLegacyDhcpServer = getEnableLegacyDhcpServer(res);
 
         provisioningApp = getResourceStringArray(res, R.array.config_mobile_hotspot_provision_app);
         provisioningAppNoUi = getResourceString(res,
@@ -222,14 +215,32 @@
                 R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip,
                 false /* defaultValue */);
 
-        // Flags should normally not be booleans, but this is a kill-switch flag that is only used
-        // to turn off the feature, so binary rollback problems do not apply.
-        mEnableSelectAllPrefixRange = getDeviceConfigBoolean(
-                TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES, true /* defaultValue */);
+        mP2pLeasesSubnetPrefixLength = getP2pLeasesSubnetPrefixLengthFromRes(res, configLog);
 
         configLog.log(toString());
     }
 
+    private int getP2pLeasesSubnetPrefixLengthFromRes(final Resources res, final SharedLog log) {
+        if (!mEnableWifiP2pDedicatedIp) return 0;
+
+        int prefixLength = getResourceInteger(res,
+                R.integer.config_p2p_leases_subnet_prefix_length, 0 /* default value */);
+
+        // DhcpLeaseRepository ignores the first and last addresses of the range so the max prefix
+        // length is 30.
+        if (prefixLength < 0 || prefixLength > 30) {
+            log.e("Invalid p2p leases subnet prefix length configuration: " + prefixLength);
+            return 0;
+        }
+
+        return prefixLength;
+    }
+
+    /** Check whether using legacy dhcp server. */
+    public boolean useLegacyDhcpServer() {
+        return mEnableLegacyDhcpServer;
+    }
+
     /** Check whether using ncm for usb tethering */
     public boolean isUsingNcm() {
         return mUsbTetheringFunction == TETHER_USB_NCM_FUNCTION;
@@ -280,6 +291,15 @@
         return mEnableWifiP2pDedicatedIp;
     }
 
+    /**
+     * Get subnet prefix length of dhcp leases for wifi p2p.
+     * This feature only support when wifi p2p use dedicated address. If
+     * #shouldEnableWifiP2pDedicatedIp is false, this method would always return 0.
+     */
+    public int getP2pLeasesSubnetPrefixLength() {
+        return mP2pLeasesSubnetPrefixLength;
+    }
+
     /** Does the dumping.*/
     public void dump(PrintWriter pw) {
         pw.print("activeDataSubId: ");
@@ -313,13 +333,13 @@
         pw.println(mEnableBpfOffload);
 
         pw.print("enableLegacyDhcpServer: ");
-        pw.println(enableLegacyDhcpServer);
+        pw.println(mEnableLegacyDhcpServer);
 
         pw.print("enableWifiP2pDedicatedIp: ");
         pw.println(mEnableWifiP2pDedicatedIp);
 
-        pw.print("mEnableSelectAllPrefixRange: ");
-        pw.println(mEnableSelectAllPrefixRange);
+        pw.print("p2pLeasesSubnetPrefixLength: ");
+        pw.println(mP2pLeasesSubnetPrefixLength);
 
         pw.print("mUsbTetheringFunction: ");
         pw.println(isUsingNcm() ? "NCM" : "RNDIS");
@@ -342,7 +362,7 @@
         sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
         sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi));
         sj.add(String.format("enableBpfOffload:%s", mEnableBpfOffload));
-        sj.add(String.format("enableLegacyDhcpServer:%s", enableLegacyDhcpServer));
+        sj.add(String.format("enableLegacyDhcpServer:%s", mEnableLegacyDhcpServer));
         return String.format("TetheringConfiguration{%s}", sj.toString());
     }
 
@@ -384,10 +404,6 @@
         return mEnableBpfOffload;
     }
 
-    public boolean isSelectAllPrefixRangeEnabled() {
-        return mEnableSelectAllPrefixRange;
-    }
-
     private int getUsbTetheringFunction(Resources res) {
         final int valueFromRes = getResourceInteger(res, R.integer.config_tether_usb_functions,
                 TETHER_USB_RNDIS_FUNCTION /* defaultValue */);
@@ -596,7 +612,7 @@
 
         parcel.legacyDhcpRanges = legacyDhcpRanges;
         parcel.defaultIPv4DNS = defaultIPv4DNS;
-        parcel.enableLegacyDhcpServer = enableLegacyDhcpServer;
+        parcel.enableLegacyDhcpServer = mEnableLegacyDhcpServer;
         parcel.provisioningApp = provisioningApp;
         parcel.provisioningAppNoUi = provisioningAppNoUi;
         parcel.provisioningCheckPeriod = provisioningCheckPeriod;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index c1a747e..9224213 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -94,13 +94,6 @@
     public abstract IpServer.Dependencies getIpServerDependencies();
 
     /**
-     * Indicates whether tethering is supported on the device.
-     */
-    public boolean isTetheringSupported() {
-        return true;
-    }
-
-    /**
      * Get a reference to the EntitlementManager to be used by tethering.
      */
     public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index 9d19335..f8dd673 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -533,8 +533,9 @@
 
         @Override
         public void onLinkPropertiesChanged(Network network, LinkProperties newLp) {
+            handleLinkProp(network, newLp);
+
             if (mCallbackType == CALLBACK_DEFAULT_INTERNET) {
-                updateLinkProperties(network, newLp);
                 // When the default network callback calls onLinkPropertiesChanged, it means that
                 // all the network information for the default network is known (because
                 // onLinkPropertiesChanged is called after onAvailable and onCapabilitiesChanged).
@@ -543,7 +544,6 @@
                 return;
             }
 
-            handleLinkProp(network, newLp);
             // Any non-LISTEN_ALL callback will necessarily concern a network that will
             // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
             // So it's not useful to do this work for non-LISTEN_ALL callbacks.
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto b/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto
new file mode 100644
index 0000000..46a47af
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+option java_multiple_files = true;
+
+package com.android.networkstack.tethering.metrics;
+
+import "frameworks/proto_logging/stats/enums/stats/connectivity/tethering.proto";
+
+/**
+ * Logs Tethering events
+ */
+message NetworkTetheringReported {
+   optional .android.stats.connectivity.ErrorCode error_code = 1;
+   optional .android.stats.connectivity.DownstreamType downstream_type = 2;
+   optional .android.stats.connectivity.UpstreamType upstream_type = 3;
+   optional .android.stats.connectivity.UserType user_type = 4;
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index 66d67a1..e6236df 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -22,7 +22,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.net.module.util.JniUtil;
-import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.net.module.util.bpf.TetherStatsValue;
 
 import java.io.FileDescriptor;
 import java.net.Inet6Address;
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index d2188d1..a4d0448 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -30,6 +30,7 @@
         "androidx.test.rules",
         "mockito-target-extended-minus-junit4",
         "net-tests-utils",
+        "net-utils-device-common-bpf",
         "testables",
     ],
     libs: [
@@ -82,68 +83,3 @@
     compile_multilib: "both",
     jarjar_rules: ":NetworkStackJarJarRules",
 }
-
-android_library {
-    name: "TetheringCoverageTestsLib",
-    min_sdk_version: "30",
-    static_libs: [
-        "NetdStaticLibTestsLib",
-        "NetworkStaticLibTestsLib",
-        "NetworkStackTestsLib",
-        "TetheringTestsLatestSdkLib",
-        "TetheringIntegrationTestsLatestSdkLib",
-    ],
-    // Jarjar rules should normally be applied on final artifacts and not intermediate libraries as
-    // applying different rules on intermediate libraries can cause conflicts when combining them
-    // (the resulting artifact can end up with multiple incompatible implementations of the same
-    // classes). But this library is used to combine tethering coverage tests with connectivity
-    // coverage tests into a single coverage target. The tests need to use the same jarjar rules as
-    // covered production code for coverage to be calculated properly, so jarjar is applied
-    // separately on each set of tests.
-    jarjar_rules: ":TetheringCoverageJarJarRules",
-    manifest: "AndroidManifest_coverage.xml",
-    visibility: [
-        "//packages/modules/Connectivity/tests:__subpackages__"
-    ],
-}
-
-// Combine NetworkStack and Tethering jarjar rules for coverage target. The jarjar files are
-// simply concatenated in the order specified in srcs.
-genrule {
-    name: "TetheringCoverageJarJarRules",
-    srcs: [
-        ":TetheringTestsJarJarRules",
-        ":NetworkStackJarJarRules",
-    ],
-    out: ["jarjar-rules-tethering-coverage.txt"],
-    cmd: "cat $(in) > $(out)",
-    visibility: ["//visibility:private"],
-}
-
-// Special version of the tethering tests that includes all tests necessary for code coverage
-// purposes. This is currently the union of TetheringTests, TetheringIntegrationTests and
-// NetworkStackTests.
-// TODO: remove in favor of ConnectivityCoverageTests, which includes below tests and more
-android_test {
-    name: "TetheringCoverageTests",
-    platform_apis: true,
-    min_sdk_version: "30",
-    target_sdk_version: "31",
-    test_suites: ["device-tests", "mts-tethering"],
-    test_config: "AndroidTest_Coverage.xml",
-    defaults: ["libnetworkstackutilsjni_deps"],
-    static_libs: [
-        "modules-utils-native-coverage-listener",
-        "TetheringCoverageTestsLib",
-    ],
-    jni_libs: [
-        // For mockito extended
-        "libdexmakerjvmtiagent",
-        "libstaticjvmtiagent",
-        // For NetworkStackUtils included in NetworkStackBase
-        "libnetworkstackutilsjni",
-        "libcom_android_networkstack_tethering_util_jni",
-    ],
-    compile_multilib: "both",
-    manifest: "AndroidManifest_coverage.xml",
-}
diff --git a/Tethering/tests/integration/AndroidManifest_coverage.xml b/Tethering/tests/integration/AndroidManifest_coverage.xml
deleted file mode 100644
index 06de00d..0000000
--- a/Tethering/tests/integration/AndroidManifest_coverage.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.networkstack.tethering.tests.coverage">
-
-    <application tools:replace="android:label"
-                 android:debuggable="true"
-                 android:label="Tethering coverage tests">
-        <uses-library android:name="android.test.runner" />
-    </application>
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.networkstack.tethering.tests.coverage"
-                     android:label="Tethering coverage tests">
-    </instrumentation>
-</manifest>
diff --git a/Tethering/tests/integration/AndroidTest_Coverage.xml b/Tethering/tests/integration/AndroidTest_Coverage.xml
deleted file mode 100644
index 33c5b3d..0000000
--- a/Tethering/tests/integration/AndroidTest_Coverage.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<configuration description="Runs coverage tests for Tethering">
-    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
-        <option name="test-file-name" value="TetheringCoverageTests.apk" />
-    </target_preparer>
-
-    <option name="test-tag" value="TetheringCoverageTests" />
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="com.android.networkstack.tethering.tests.coverage" />
-        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
-        <option name="hidden-api-checks" value="false"/>
-        <option name="device-listeners" value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
-    </test>
-</configuration>
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 8bf1a2b..de81a38 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
 import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.DUMP;
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.TETHER_PRIVILEGED;
@@ -53,11 +54,15 @@
 import android.net.TetheringManager.TetheringEventCallback;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringTester.TetheredDevice;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Base64;
 import android.util.Log;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -67,17 +72,24 @@
 
 import com.android.net.module.util.PacketBuilder;
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.bpf.Tether4Key;
+import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.UdpHeader;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DumpTestUtils;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TapPacketReader;
 import com.android.testutils.TestNetworkTracker;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -88,9 +100,13 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
@@ -101,10 +117,17 @@
 @RunWith(AndroidJUnit4.class)
 @MediumTest
 public class EthernetTetheringTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
     private static final String TAG = EthernetTetheringTest.class.getSimpleName();
     private static final int TIMEOUT_MS = 5000;
     private static final int TETHER_REACHABILITY_ATTEMPTS = 20;
+    private static final int DUMP_POLLING_MAX_RETRY = 100;
+    private static final int DUMP_POLLING_INTERVAL_MS = 50;
+    // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
+    // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
+    private static final int UDP_STREAM_TS_MS = 2000;
     private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
     private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
     private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8");
@@ -112,6 +135,10 @@
     private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
             ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
 
+    private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
+    private static final String BASE64_DELIMITER = ",";
+    private static final String LINE_DELIMITER = "\\n";
+
     private final Context mContext = InstrumentationRegistry.getContext();
     private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
     private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
@@ -136,10 +163,11 @@
         // Needed to create a TestNetworkInterface, to call requestTetheredInterface, and to receive
         // tethered client callbacks. The restricted networks permission is needed to ensure that
         // EthernetManager#isAvailable will correctly return true on devices where Ethernet is
-        // marked restricted, like cuttlefish.
+        // marked restricted, like cuttlefish. The dump permission is needed to verify bpf related
+        // functions via dumpsys output.
         mUiAutomation.adoptShellPermissionIdentity(
                 MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED, ACCESS_NETWORK_STATE,
-                CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+                CONNECTIVITY_USE_RESTRICTED_NETWORKS, DUMP);
         mRunTests = mTm.isTetheringSupported() && mEm != null;
         assumeTrue(mRunTests);
 
@@ -251,7 +279,7 @@
         final String localAddr = "192.0.2.3/28";
         final String clientAddr = "192.0.2.2/28";
         mTetheringEventCallback = enableEthernetTethering(iface,
-                requestWithStaticIpv4(localAddr, clientAddr));
+                requestWithStaticIpv4(localAddr, clientAddr), null /* any upstream */);
 
         mTetheringEventCallback.awaitInterfaceTethered();
         assertInterfaceHasIpAddress(iface, localAddr);
@@ -334,7 +362,8 @@
 
         final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build();
-        mTetheringEventCallback = enableEthernetTethering(iface, request);
+        mTetheringEventCallback = enableEthernetTethering(iface, request,
+                null /* any upstream */);
         mTetheringEventCallback.awaitInterfaceLocalOnly();
 
         // makePacketReader only works after tethering is started, because until then the interface
@@ -365,7 +394,7 @@
         final String iface = mTetheredInterfaceRequester.getInterface();
 
         // Enable Ethernet tethering and check that it starts.
-        mTetheringEventCallback = enableEthernetTethering(iface);
+        mTetheringEventCallback = enableEthernetTethering(iface, null /* any upstream */);
 
         // There is nothing more we can do on a physical interface without connecting an actual
         // client, which is not possible in this test.
@@ -378,8 +407,11 @@
         private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
         private final CountDownLatch mLocalOnlyStoppedLatch = new CountDownLatch(1);
         private final CountDownLatch mClientConnectedLatch = new CountDownLatch(1);
-        private final CountDownLatch mUpstreamConnectedLatch = new CountDownLatch(1);
+        private final CountDownLatch mUpstreamLatch = new CountDownLatch(1);
         private final TetheringInterface mIface;
+        private final Network mExpectedUpstream;
+
+        private boolean mAcceptAnyUpstream = false;
 
         private volatile boolean mInterfaceWasTethered = false;
         private volatile boolean mInterfaceWasLocalOnly = false;
@@ -388,8 +420,14 @@
         private volatile Network mUpstream = null;
 
         MyTetheringEventCallback(TetheringManager tm, String iface) {
+            this(tm, iface, null);
+            mAcceptAnyUpstream = true;
+        }
+
+        MyTetheringEventCallback(TetheringManager tm, String iface, Network expectedUpstream) {
             mTm = tm;
             mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+            mExpectedUpstream = expectedUpstream;
         }
 
         public void unregister() {
@@ -499,19 +537,30 @@
 
             Log.d(TAG, "Got upstream changed: " + network);
             mUpstream = network;
-            if (mUpstream != null) mUpstreamConnectedLatch.countDown();
+            if (mAcceptAnyUpstream || Objects.equals(mUpstream, mExpectedUpstream)) {
+                mUpstreamLatch.countDown();
+            }
         }
 
-        public Network awaitFirstUpstreamConnected() throws Exception {
-            assertTrue("Did not receive upstream connected callback after " + TIMEOUT_MS + "ms",
-                    mUpstreamConnectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        public Network awaitUpstreamChanged() throws Exception {
+            if (!mUpstreamLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                fail("Did not receive upstream " + (mAcceptAnyUpstream ? "any" : mExpectedUpstream)
+                        + " callback after " + TIMEOUT_MS + "ms");
+            }
             return mUpstream;
         }
     }
 
     private MyTetheringEventCallback enableEthernetTethering(String iface,
-            TetheringRequest request) throws Exception {
-        MyTetheringEventCallback callback = new MyTetheringEventCallback(mTm, iface);
+            TetheringRequest request, Network expectedUpstream) throws Exception {
+        // Enable ethernet tethering with null expectedUpstream means the test accept any upstream
+        // after etherent tethering started.
+        final MyTetheringEventCallback callback;
+        if (expectedUpstream != null) {
+            callback = new MyTetheringEventCallback(mTm, iface, expectedUpstream);
+        } else {
+            callback = new MyTetheringEventCallback(mTm, iface);
+        }
         mTm.registerTetheringEventCallback(mHandler::post, callback);
 
         StartTetheringCallback startTetheringCallback = new StartTetheringCallback() {
@@ -538,10 +587,11 @@
         return callback;
     }
 
-    private MyTetheringEventCallback enableEthernetTethering(String iface) throws Exception {
+    private MyTetheringEventCallback enableEthernetTethering(String iface, Network expectedUpstream)
+            throws Exception {
         return enableEthernetTethering(iface,
                 new TetheringRequest.Builder(TETHERING_ETHERNET)
-                .setShouldShowEntitlementUi(false).build());
+                .setShouldShowEntitlementUi(false).build(), expectedUpstream);
     }
 
     private int getMTU(TestNetworkInterface iface) throws SocketException {
@@ -565,7 +615,8 @@
     private void checkVirtualEthernet(TestNetworkInterface iface, int mtu) throws Exception {
         FileDescriptor fd = iface.getFileDescriptor().getFileDescriptor();
         mDownstreamReader = makePacketReader(fd, mtu);
-        mTetheringEventCallback = enableEthernetTethering(iface.getInterfaceName());
+        mTetheringEventCallback = enableEthernetTethering(iface.getInterfaceName(),
+                null /* any upstream */);
         checkTetheredClientCallbacks(mDownstreamReader);
     }
 
@@ -663,7 +714,8 @@
     private void assertInvalidStaticIpv4Request(String iface, String local, String client)
             throws Exception {
         try {
-            enableEthernetTethering(iface, requestWithStaticIpv4(local, client));
+            enableEthernetTethering(iface, requestWithStaticIpv4(local, client),
+                    null /* any upstream */);
             fail("Unexpectedly accepted invalid IPv4 configuration: " + local + ", " + client);
         } catch (IllegalArgumentException | NullPointerException expected) { }
     }
@@ -719,9 +771,10 @@
         assertEquals("TetheredInterfaceCallback for unexpected interface",
                 mDownstreamIface.getInterfaceName(), iface);
 
-        mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName());
+        mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
+                mUpstreamTracker.getNetwork());
         assertEquals("onUpstreamChanged for unexpected network", mUpstreamTracker.getNetwork(),
-                mTetheringEventCallback.awaitFirstUpstreamConnected());
+                mTetheringEventCallback.awaitUpstreamChanged());
 
         mDownstreamReader = makePacketReader(mDownstreamIface);
         // TODO: do basic forwarding test here.
@@ -747,12 +800,15 @@
     private static final byte TYPE_OF_SERVICE = 0;
     private static final short ID = 27149;
     private static final short ID2 = 27150;
+    private static final short ID3 = 27151;
     private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
     private static final byte TIME_TO_LIVE = (byte) 0x40;
     private static final ByteBuffer PAYLOAD =
             ByteBuffer.wrap(new byte[] { (byte) 0x12, (byte) 0x34 });
     private static final ByteBuffer PAYLOAD2 =
             ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
+    private static final ByteBuffer PAYLOAD3 =
+            ByteBuffer.wrap(new byte[] { (byte) 0x9a, (byte) 0xbc });
 
     private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
             @NonNull final ByteBuffer payload) {
@@ -830,7 +886,8 @@
         return false;
     }
 
-    private void runUdp4Test(TetheringTester tester, RemoteResponder remote) throws Exception {
+    private void runUdp4Test(TetheringTester tester, RemoteResponder remote, boolean usingBpf)
+            throws Exception {
         final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
                 "1:2:3:4:5:6"));
 
@@ -861,10 +918,51 @@
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
         });
+
+        if (usingBpf) {
+            // Send second UDP packet in original direction.
+            // The BPF coordinator only offloads the ASSURED conntrack entry. The "request + reply"
+            // packets can make status IPS_SEEN_REPLY to be set. Need one more packet to make
+            // conntrack status IPS_ASSURED_BIT to be set. Note the third packet needs to delay
+            // 2 seconds because kernel monitors a UDP connection which still alive after 2 seconds
+            // and apply ASSURED flag.
+            // See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
+            // nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
+            Thread.sleep(UDP_STREAM_TS_MS);
+            final ByteBuffer originalPacket2 = buildUdpv4Packet(tethered.macAddr,
+                    tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
+                    REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */,
+                    REMOTE_PORT /*dstPort */, PAYLOAD3 /* payload */);
+            tester.verifyUpload(remote, originalPacket2, p -> {
+                Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+                return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD3);
+            });
+
+            final HashMap<Tether4Key, Tether4Value> upstreamMap = pollIpv4UpstreamMapFromDump();
+            assertNotNull(upstreamMap);
+            assertEquals(1, upstreamMap.size());
+
+            final Map.Entry<Tether4Key, Tether4Value> rule =
+                    upstreamMap.entrySet().iterator().next();
+
+            final Tether4Key key = rule.getKey();
+            assertEquals(IPPROTO_UDP, key.l4proto);
+            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), key.src4));
+            assertEquals(LOCAL_PORT, key.srcPort);
+            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), key.dst4));
+            assertEquals(REMOTE_PORT, key.dstPort);
+
+            final Tether4Value value = rule.getValue();
+            assertTrue(Arrays.equals(publicIp4Addr.getAddress(),
+                    InetAddress.getByAddress(value.src46).getAddress()));
+            assertEquals(LOCAL_PORT, value.srcPort);
+            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
+                    InetAddress.getByAddress(value.dst46).getAddress()));
+            assertEquals(REMOTE_PORT, value.dstPort);
+        }
     }
 
-    @Test
-    public void testUdpV4() throws Exception {
+    void initializeTethering() throws Exception {
         assumeFalse(mEm.isAvailable());
 
         // MyTetheringEventCallback currently only support await first available upstream. Tethering
@@ -879,14 +977,82 @@
         assertEquals("TetheredInterfaceCallback for unexpected interface",
                 mDownstreamIface.getInterfaceName(), iface);
 
-        mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName());
+        mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
+                mUpstreamTracker.getNetwork());
         assertEquals("onUpstreamChanged for unexpected network", mUpstreamTracker.getNetwork(),
-                mTetheringEventCallback.awaitFirstUpstreamConnected());
+                mTetheringEventCallback.awaitUpstreamChanged());
 
         mDownstreamReader = makePacketReader(mDownstreamIface);
         mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
+    }
 
-        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader));
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.Q)
+    public void testTetherUdpV4WithoutBpf() throws Exception {
+        initializeTethering();
+        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
+                false /* usingBpf */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testTetherUdpV4WithBpf() throws Exception {
+        initializeTethering();
+        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
+                true /* usingBpf */);
+    }
+
+    @Nullable
+    private Pair<Tether4Key, Tether4Value> parseTether4KeyValue(@NonNull String dumpStr) {
+        Log.w(TAG, "Parsing string: " + dumpStr);
+
+        String[] keyValueStrs = dumpStr.split(BASE64_DELIMITER);
+        if (keyValueStrs.length != 2 /* key + value */) {
+            fail("The length is " + keyValueStrs.length + " but expect 2. "
+                    + "Split string(s): " + TextUtils.join(",", keyValueStrs));
+        }
+
+        final byte[] keyBytes = Base64.decode(keyValueStrs[0], Base64.DEFAULT);
+        Log.d(TAG, "keyBytes: " + dumpHexString(keyBytes));
+        final ByteBuffer keyByteBuffer = ByteBuffer.wrap(keyBytes);
+        keyByteBuffer.order(ByteOrder.nativeOrder());
+        final Tether4Key tether4Key = Struct.parse(Tether4Key.class, keyByteBuffer);
+        Log.w(TAG, "tether4Key: " + tether4Key);
+
+        final byte[] valueBytes = Base64.decode(keyValueStrs[1], Base64.DEFAULT);
+        Log.d(TAG, "valueBytes: " + dumpHexString(valueBytes));
+        final ByteBuffer valueByteBuffer = ByteBuffer.wrap(valueBytes);
+        valueByteBuffer.order(ByteOrder.nativeOrder());
+        final Tether4Value tether4Value = Struct.parse(Tether4Value.class, valueByteBuffer);
+        Log.w(TAG, "tether4Value: " + tether4Value);
+
+        return new Pair<>(tether4Key, tether4Value);
+    }
+
+    @NonNull
+    private HashMap<Tether4Key, Tether4Value> dumpIpv4UpstreamMap() throws Exception {
+        final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE,
+                DUMPSYS_TETHERING_RAWMAP_ARG);
+        final HashMap<Tether4Key, Tether4Value> map = new HashMap<>();
+
+        for (final String line : rawMapStr.split(LINE_DELIMITER)) {
+            final Pair<Tether4Key, Tether4Value> rule = parseTether4KeyValue(line.trim());
+            map.put(rule.first, rule.second);
+        }
+        return map;
+    }
+
+    @Nullable
+    private HashMap<Tether4Key, Tether4Value> pollIpv4UpstreamMapFromDump() throws Exception {
+        for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
+            final HashMap<Tether4Key, Tether4Value> map = dumpIpv4UpstreamMap();
+            if (!map.isEmpty()) return map;
+
+            Thread.sleep(DUMP_POLLING_INTERVAL_MS);
+        }
+
+        fail("Cannot get rules after " + DUMP_POLLING_MAX_RETRY * DUMP_POLLING_INTERVAL_MS + "ms");
+        return null;
     }
 
     private <T> List<T> toList(T... array) {
diff --git a/Tethering/tests/jarjar-rules.txt b/Tethering/tests/jarjar-rules.txt
index a7c7488..cd8fd3a 100644
--- a/Tethering/tests/jarjar-rules.txt
+++ b/Tethering/tests/jarjar-rules.txt
@@ -7,6 +7,8 @@
 rule com.android.internal.util.State* com.android.networkstack.tethering.util.State@1
 rule com.android.internal.util.StateMachine* com.android.networkstack.tethering.util.StateMachine@1
 rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1
+# Keep other com.android.internal.util as-is
+rule com.android.internal.util.** @0
 
 rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1
 
diff --git a/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
index ef254ff..4525568 100644
--- a/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
+++ b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
@@ -29,7 +29,6 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 
 import android.app.UiAutomation;
 import android.content.Context;
@@ -81,12 +80,8 @@
         mUiAutomation.dropShellPermissionIdentity();
     }
 
-    private static final String TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES =
-            "tether_enable_select_all_prefix_ranges";
     @Test
     public void testSwitchBasePrefixRangeWhenConflict() throws Exception {
-        assumeTrue(isFeatureEnabled(TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES, true));
-
         addressConflictTest(true);
     }
 
diff --git a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
index eb9cf71..ebf09ed 100644
--- a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
@@ -30,7 +30,6 @@
 import android.net.INetd;
 import android.net.InetAddresses;
 import android.net.MacAddress;
-import android.net.util.InterfaceParams;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -40,6 +39,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.net.module.util.InterfaceParams;
 import com.android.networkstack.tethering.util.TetheringUtils;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index 34f3e0e..328e3fb 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.util.InterfaceParams;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -54,6 +53,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.Struct;
diff --git a/Tethering/tests/privileged/src/com/android/net/module/util/BpfBitmapTest.java b/Tethering/tests/privileged/src/com/android/net/module/util/BpfBitmapTest.java
new file mode 100644
index 0000000..2112396
--- /dev/null
+++ b/Tethering/tests/privileged/src/com/android/net/module/util/BpfBitmapTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import static com.android.networkstack.tethering.util.TetheringUtils.getTetheringJniLibraryName;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.net.module.util.BpfBitmap;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+public final class BpfBitmapTest {
+    private static final String TEST_BITMAP_PATH =
+            "/sys/fs/bpf/tethering/map_test_bitmap";
+
+    private static final int mTestData[] = {0,1,2,6,63,64,72};
+    private BpfBitmap mTestBitmap;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestBitmap = new BpfBitmap(TEST_BITMAP_PATH);
+        mTestBitmap.clear();
+        assertTrue(mTestBitmap.isEmpty());
+    }
+
+    @Test
+    public void testSet() throws Exception {
+        for (int i : mTestData) {
+            mTestBitmap.set(i);
+            assertFalse(mTestBitmap.isEmpty());
+            assertTrue(mTestBitmap.get(i));
+            // Check that the next item in the bitmap is unset since test data is in
+            // ascending order.
+            assertFalse(mTestBitmap.get(i + 1));
+        }
+    }
+
+    @Test
+    public void testSetThenUnset() throws Exception {
+        for (int i : mTestData) {
+            mTestBitmap.set(i);
+            assertFalse(mTestBitmap.isEmpty());
+            assertTrue(mTestBitmap.get(i));
+            // Since test unsets all test data during each iteration, ensure all other
+            // bit are unset.
+            for (int j = 0; j < 128; ++j) if (j != i) assertFalse(mTestBitmap.get(j));
+            mTestBitmap.unset(i);
+        }
+    }
+
+    @Test
+    public void testSetAllThenUnsetAll() throws Exception {
+        for (int i : mTestData) {
+            mTestBitmap.set(i);
+        }
+
+        for (int i : mTestData) {
+            mTestBitmap.unset(i);
+            if (i < mTestData.length)
+                assertFalse(mTestBitmap.isEmpty());
+            assertFalse(mTestBitmap.get(i));
+        }
+        assertTrue(mTestBitmap.isEmpty());
+    }
+
+    @Test
+    public void testClear() throws Exception {
+        for (int i = 0; i < 128; ++i) {
+            mTestBitmap.set(i);
+        }
+        assertFalse(mTestBitmap.isEmpty());
+        mTestBitmap.clear();
+        assertTrue(mTestBitmap.isEmpty());
+    }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index ecd1a39..d1b8380 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -34,6 +34,7 @@
     libs: [
         "framework-minus-apex",
         "framework-connectivity.impl",
+        "framework-connectivity-t.impl",
         "framework-tethering.impl",
     ],
     visibility: [
@@ -67,6 +68,7 @@
         "framework-minus-apex",
         "framework-res",
         "framework-connectivity.impl",
+        "framework-connectivity-t.impl",
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
     ],
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index a3c46c2..aac531a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -86,7 +86,6 @@
 import android.net.ip.IpNeighborMonitor.NeighborEvent;
 import android.net.ip.IpNeighborMonitor.NeighborEventConsumer;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
@@ -100,9 +99,12 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
@@ -112,8 +114,6 @@
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherStatsKey;
-import com.android.networkstack.tethering.TetherStatsValue;
 import com.android.networkstack.tethering.TetherUpstream6Key;
 import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.networkstack.tethering.util.InterfaceSet;
@@ -156,6 +156,8 @@
     private static final int BLUETOOTH_DHCP_PREFIX_LENGTH = 24;
     private static final int DHCP_LEASE_TIME_SECS = 3600;
     private static final boolean DEFAULT_USING_BPF_OFFLOAD = true;
+    private static final int DEFAULT_SUBNET_PREFIX_LENGTH = 0;
+    private static final int P2P_SUBNET_PREFIX_LENGTH = 25;
 
     private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams(
             IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
@@ -228,9 +230,12 @@
         doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(),
                 neighborCaptor.capture());
 
+        when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
+        when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
+        when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
         mIpServer = new IpServer(
                 IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
-                mCallback, usingLegacyDhcp, usingBpfOffload, mAddressCoordinator, mDependencies);
+                mCallback, mTetherConfig, mAddressCoordinator, mDependencies);
         mIpServer.start();
         mNeighborEventConsumer = neighborCaptor.getValue();
 
@@ -281,7 +286,8 @@
         when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog);
         when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
                 mTestAddress);
-        when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
+        when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
+        when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
 
         mBpfDeps = new BpfCoordinator.Dependencies() {
                     @NonNull
@@ -360,8 +366,8 @@
         when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
                 .thenReturn(mIpNeighborMonitor);
         mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
-                mNetd, mBpfCoordinator, mCallback, false /* usingLegacyDhcp */,
-                DEFAULT_USING_BPF_OFFLOAD, mAddressCoordinator, mDependencies);
+                mNetd, mBpfCoordinator, mCallback, mTetherConfig, mAddressCoordinator,
+                mDependencies);
         mIpServer.start();
         mLooper.dispatchAll();
         verify(mCallback).updateInterfaceState(
@@ -1309,6 +1315,12 @@
         if (mIpServer.interfaceType() == TETHERING_NCM) {
             assertTrue(params.changePrefixOnDecline);
         }
+
+        if (mIpServer.interfaceType() == TETHERING_WIFI_P2P) {
+            assertEquals(P2P_SUBNET_PREFIX_LENGTH, params.leasesSubnetPrefixLength);
+        } else {
+            assertEquals(DEFAULT_SUBNET_PREFIX_LENGTH, params.leasesSubnetPrefixLength);
+        }
     }
 
     private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 6c7a66d..3630f24 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -47,6 +47,7 @@
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
+import static com.android.networkstack.tethering.BpfCoordinator.toIpv4MappedAddressBytes;
 import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
 import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
@@ -84,7 +85,6 @@
 import android.net.ip.ConntrackMonitor;
 import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
-import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
@@ -98,9 +98,12 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
@@ -129,6 +132,7 @@
 import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 
 @RunWith(AndroidJUnit4.class)
@@ -138,22 +142,215 @@
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
     private static final int TEST_NET_ID = 24;
+    private static final int TEST_NET_ID2 = 25;
 
+    private static final int INVALID_IFINDEX = 0;
     private static final int UPSTREAM_IFINDEX = 1001;
-    private static final int DOWNSTREAM_IFINDEX = 1002;
+    private static final int UPSTREAM_IFINDEX2 = 1002;
+    private static final int DOWNSTREAM_IFINDEX = 1003;
+    private static final int DOWNSTREAM_IFINDEX2 = 1004;
 
     private static final String UPSTREAM_IFACE = "rmnet0";
+    private static final String UPSTREAM_IFACE2 = "wlan0";
 
     private static final MacAddress DOWNSTREAM_MAC = MacAddress.fromString("12:34:56:78:90:ab");
+    private static final MacAddress DOWNSTREAM_MAC2 = MacAddress.fromString("ab:90:78:56:34:12");
+
     private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
     private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
 
     private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
     private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
 
+    private static final Inet4Address REMOTE_ADDR =
+            (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+    private static final Inet4Address PUBLIC_ADDR =
+            (Inet4Address) InetAddresses.parseNumericAddress("1.0.0.1");
+    private static final Inet4Address PUBLIC_ADDR2 =
+            (Inet4Address) InetAddresses.parseNumericAddress("1.0.0.2");
+    private static final Inet4Address PRIVATE_ADDR =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+    private static final Inet4Address PRIVATE_ADDR2 =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.168.90.12");
+
+    // Generally, public port and private port are the same in the NAT conntrack message.
+    // TODO: consider using different private port and public port for testing.
+    private static final short REMOTE_PORT = (short) 443;
+    private static final short PUBLIC_PORT = (short) 62449;
+    private static final short PUBLIC_PORT2 = (short) 62450;
+    private static final short PRIVATE_PORT = (short) 62449;
+    private static final short PRIVATE_PORT2 = (short) 62450;
+
     private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams(
             UPSTREAM_IFACE, UPSTREAM_IFINDEX, null /* macAddr, rawip */,
             NetworkStackConstants.ETHER_MTU);
+    private static final InterfaceParams UPSTREAM_IFACE_PARAMS2 = new InterfaceParams(
+            UPSTREAM_IFACE2, UPSTREAM_IFINDEX2, MacAddress.fromString("44:55:66:00:00:0c"),
+            NetworkStackConstants.ETHER_MTU);
+
+    private static final HashMap<Integer, UpstreamInformation> UPSTREAM_INFORMATIONS =
+            new HashMap<Integer, UpstreamInformation>() {{
+                    put(UPSTREAM_IFINDEX, new UpstreamInformation(UPSTREAM_IFACE_PARAMS,
+                            PUBLIC_ADDR, NetworkCapabilities.TRANSPORT_CELLULAR, TEST_NET_ID));
+                    put(UPSTREAM_IFINDEX2, new UpstreamInformation(UPSTREAM_IFACE_PARAMS2,
+                            PUBLIC_ADDR2, NetworkCapabilities.TRANSPORT_WIFI, TEST_NET_ID2));
+            }};
+
+    private static final ClientInfo CLIENT_INFO_A = new ClientInfo(DOWNSTREAM_IFINDEX,
+            DOWNSTREAM_MAC, PRIVATE_ADDR, MAC_A);
+    private static final ClientInfo CLIENT_INFO_B = new ClientInfo(DOWNSTREAM_IFINDEX2,
+            DOWNSTREAM_MAC2, PRIVATE_ADDR2, MAC_B);
+
+    private static class UpstreamInformation {
+        public final InterfaceParams interfaceParams;
+        public final Inet4Address address;
+        public final int transportType;
+        public final int netId;
+
+        UpstreamInformation(final InterfaceParams interfaceParams,
+                final Inet4Address address, int transportType, int netId) {
+            this.interfaceParams = interfaceParams;
+            this.address = address;
+            this.transportType = transportType;
+            this.netId = netId;
+        }
+    }
+
+    private static class TestUpstream4Key {
+        public static class Builder {
+            private long mIif = DOWNSTREAM_IFINDEX;
+            private MacAddress mDstMac = DOWNSTREAM_MAC;
+            private short mL4proto = (short) IPPROTO_TCP;
+            private byte[] mSrc4 = PRIVATE_ADDR.getAddress();
+            private byte[] mDst4 = REMOTE_ADDR.getAddress();
+            private int mSrcPort = PRIVATE_PORT;
+            private int mDstPort = REMOTE_PORT;
+
+            public Builder setProto(int proto) {
+                if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+                    fail("Not support protocol " + proto);
+                }
+                mL4proto = (short) proto;
+                return this;
+            }
+
+            public Tether4Key build() {
+                return new Tether4Key(mIif, mDstMac, mL4proto, mSrc4, mDst4, mSrcPort, mDstPort);
+            }
+        }
+    }
+
+    private static class TestDownstream4Key {
+        public static class Builder {
+            private long mIif = UPSTREAM_IFINDEX;
+            private MacAddress mDstMac = MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */;
+            private short mL4proto = (short) IPPROTO_TCP;
+            private byte[] mSrc4 = REMOTE_ADDR.getAddress();
+            private byte[] mDst4 = PUBLIC_ADDR.getAddress();
+            private int mSrcPort = REMOTE_PORT;
+            private int mDstPort = PUBLIC_PORT;
+
+            public Builder setProto(int proto) {
+                if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+                    fail("Not support protocol " + proto);
+                }
+                mL4proto = (short) proto;
+                return this;
+            }
+
+            public Tether4Key build() {
+                return new Tether4Key(mIif, mDstMac, mL4proto, mSrc4, mDst4, mSrcPort, mDstPort);
+            }
+        }
+    }
+
+    private static class TestUpstream4Value {
+        public static class Builder {
+            private long mOif = UPSTREAM_IFINDEX;
+            private MacAddress mEthDstMac = MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */;
+            private MacAddress mEthSrcMac = MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */;
+            private int mEthProto = ETH_P_IP;
+            private short mPmtu = NetworkStackConstants.ETHER_MTU;
+            private byte[] mSrc46 = toIpv4MappedAddressBytes(PUBLIC_ADDR);
+            private byte[] mDst46 = toIpv4MappedAddressBytes(REMOTE_ADDR);
+            private int mSrcPort = PUBLIC_PORT;
+            private int mDstPort = REMOTE_PORT;
+            private long mLastUsed = 0;
+
+            public Tether4Value build() {
+                return new Tether4Value(mOif, mEthDstMac, mEthSrcMac, mEthProto, mPmtu,
+                        mSrc46, mDst46, mSrcPort, mDstPort, mLastUsed);
+            }
+        }
+    }
+
+    private static class TestDownstream4Value {
+        public static class Builder {
+            private long mOif = DOWNSTREAM_IFINDEX;
+            private MacAddress mEthDstMac = MAC_A /* client mac */;
+            private MacAddress mEthSrcMac = DOWNSTREAM_MAC;
+            private int mEthProto = ETH_P_IP;
+            private short mPmtu = NetworkStackConstants.ETHER_MTU;
+            private byte[] mSrc46 = toIpv4MappedAddressBytes(REMOTE_ADDR);
+            private byte[] mDst46 = toIpv4MappedAddressBytes(PRIVATE_ADDR);
+            private int mSrcPort = REMOTE_PORT;
+            private int mDstPort = PRIVATE_PORT;
+            private long mLastUsed = 0;
+
+            public Tether4Value build() {
+                return new Tether4Value(mOif, mEthDstMac, mEthSrcMac, mEthProto, mPmtu,
+                        mSrc46, mDst46, mSrcPort, mDstPort, mLastUsed);
+            }
+        }
+    }
+
+    private static class TestConntrackEvent {
+        public static class Builder {
+            private short mMsgType = IPCTNL_MSG_CT_NEW;
+            private short mProto = (short) IPPROTO_TCP;
+            private Inet4Address mPrivateAddr = PRIVATE_ADDR;
+            private Inet4Address mPublicAddr = PUBLIC_ADDR;
+            private Inet4Address mRemoteAddr = REMOTE_ADDR;
+            private short mPrivatePort = PRIVATE_PORT;
+            private short mPublicPort = PUBLIC_PORT;
+            private short mRemotePort = REMOTE_PORT;
+
+            public Builder setMsgType(short msgType) {
+                if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) {
+                    fail("Not support message type " + msgType);
+                }
+                mMsgType = (short) msgType;
+                return this;
+            }
+
+            public Builder setProto(int proto) {
+                if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+                    fail("Not support protocol " + proto);
+                }
+                mProto = (short) proto;
+                return this;
+            }
+
+            public Builder setRemotePort(int remotePort) {
+                mRemotePort = (short) remotePort;
+                return this;
+            }
+
+            public ConntrackEvent build() {
+                final int status = (mMsgType == IPCTNL_MSG_CT_NEW) ? ESTABLISHED_MASK : DYING_MASK;
+                final int timeoutSec = (mMsgType == IPCTNL_MSG_CT_NEW) ? 100 /* nonzero, new */
+                        : 0 /* unused, delete */;
+                return new ConntrackEvent(
+                        (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | mMsgType),
+                        new Tuple(new TupleIpv4(mPrivateAddr, mRemoteAddr),
+                                new TupleProto((byte) mProto, mPrivatePort, mRemotePort)),
+                        new Tuple(new TupleIpv4(mRemoteAddr, mPublicAddr),
+                                new TupleProto((byte) mProto, mRemotePort, mPublicPort)),
+                        status,
+                        timeoutSec);
+            }
+        }
+    }
 
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private INetd mNetd;
@@ -161,8 +358,6 @@
     @Mock private IpServer mIpServer2;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
-    @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
-    @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
     @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
     @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
     @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
@@ -174,11 +369,16 @@
     // Late init since the object must be initialized by the BPF coordinator instance because
     // it has to access the non-static function of BPF coordinator.
     private BpfConntrackEventConsumer mConsumer;
+    private HashMap<IpServer, HashMap<Inet4Address, ClientInfo>> mTetherClients;
 
     private long mElapsedRealtimeNanos = 0;
     private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
             ArgumentCaptor.forClass(ArrayList.class);
     private final TestLooper mTestLooper = new TestLooper();
+    private final BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map =
+            spy(new TestBpfMap<>(Tether4Key.class, Tether4Value.class));
+    private final BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map =
+            spy(new TestBpfMap<>(Tether4Key.class, Tether4Value.class));
     private final TestBpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap =
             spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class));
     private final TestBpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap =
@@ -275,6 +475,8 @@
         final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
 
         mConsumer = coordinator.getBpfConntrackEventConsumerForTesting();
+        mTetherClients = coordinator.getTetherClientsForTesting();
+
         final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider>
                 tetherStatsProviderCaptor =
                 ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class);
@@ -1244,140 +1446,67 @@
     // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
     // +------------+         +------------+------------+         +------------+
     // remote ip              public ip                           private ip
-    // 140.112.8.116:443      100.81.179.1:62449                  192.168.80.12:62449
+    // 140.112.8.116:443      1.0.0.1:62449                       192.168.80.12:62449
     //
-    private static final Inet4Address REMOTE_ADDR =
-            (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
-    private static final Inet4Address PUBLIC_ADDR =
-            (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
-    private static final Inet4Address PRIVATE_ADDR =
-            (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
 
-    // IPv4-mapped IPv6 addresses
-    // Remote addrress ::ffff:140.112.8.116
-    // Public addrress ::ffff:100.81.179.1
-    // Private addrress ::ffff:192.168.80.12
-    private static final byte[] REMOTE_ADDR_V4MAPPED_BYTES = new byte[] {
-            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
-            (byte) 0x8c, (byte) 0x70, (byte) 0x08, (byte) 0x74 };
-    private static final byte[] PUBLIC_ADDR_V4MAPPED_BYTES = new byte[] {
-            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
-            (byte) 0x64, (byte) 0x51, (byte) 0xb3, (byte) 0x01 };
-    private static final byte[] PRIVATE_ADDR_V4MAPPED_BYTES = new byte[] {
-            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
-            (byte) 0xc0, (byte) 0xa8, (byte) 0x50, (byte) 0x0c };
-
-    // Generally, public port and private port are the same in the NAT conntrack message.
-    // TODO: consider using different private port and public port for testing.
-    private static final short REMOTE_PORT = (short) 443;
-    private static final short PUBLIC_PORT = (short) 62449;
-    private static final short PRIVATE_PORT = (short) 62449;
-
-    @NonNull
-    private Tether4Key makeUpstream4Key(int proto) {
-        if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
-            fail("Not support protocol " + proto);
-        }
-        return new Tether4Key(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, (short) proto,
-            PRIVATE_ADDR.getAddress(), REMOTE_ADDR.getAddress(), PRIVATE_PORT, REMOTE_PORT);
-    }
-
-    @NonNull
-    private Tether4Key makeDownstream4Key(int proto) {
-        if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
-            fail("Not support protocol " + proto);
-        }
-        return new Tether4Key(UPSTREAM_IFINDEX,
-                MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */, (short) proto,
-                REMOTE_ADDR.getAddress(), PUBLIC_ADDR.getAddress(), REMOTE_PORT, PUBLIC_PORT);
-    }
-
-    @NonNull
-    private Tether4Value makeUpstream4Value() {
-        return new Tether4Value(UPSTREAM_IFINDEX,
-                MacAddress.ALL_ZEROS_ADDRESS /* ethDstMac (rawip) */,
-                MacAddress.ALL_ZEROS_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP,
-                NetworkStackConstants.ETHER_MTU, PUBLIC_ADDR_V4MAPPED_BYTES,
-                REMOTE_ADDR_V4MAPPED_BYTES, PUBLIC_PORT, REMOTE_PORT, 0 /* lastUsed */);
-    }
-
-    @NonNull
-    private Tether4Value makeDownstream4Value() {
-        return new Tether4Value(DOWNSTREAM_IFINDEX, MAC_A /* client mac */, DOWNSTREAM_MAC,
-                ETH_P_IP, NetworkStackConstants.ETHER_MTU, REMOTE_ADDR_V4MAPPED_BYTES,
-                PRIVATE_ADDR_V4MAPPED_BYTES, REMOTE_PORT, PRIVATE_PORT, 0 /* lastUsed */);
-    }
-
-    @NonNull
-    private Tether4Key makeDownstream4Key() {
-        return makeDownstream4Key(IPPROTO_TCP);
-    }
-
-    @NonNull
-    private ConntrackEvent makeTestConntrackEvent(short msgType, int proto, short remotePort) {
-        if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) {
-            fail("Not support message type " + msgType);
-        }
-        if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
-            fail("Not support protocol " + proto);
+    // Setup upstream interface to BpfCoordinator.
+    //
+    // @param coordinator BpfCoordinator instance.
+    // @param upstreamIfindex upstream interface index. can be the following values.
+    //        INVALID_IFINDEX: no upstream interface
+    //        UPSTREAM_IFINDEX: CELLULAR (raw ip interface)
+    //        UPSTREAM_IFINDEX2: WIFI (ethernet interface)
+    private void setUpstreamInformationTo(final BpfCoordinator coordinator,
+            @Nullable Integer upstreamIfindex) {
+        if (upstreamIfindex == INVALID_IFINDEX) {
+            coordinator.updateUpstreamNetworkState(null);
+            return;
         }
 
-        final int status = (msgType == IPCTNL_MSG_CT_NEW) ? ESTABLISHED_MASK : DYING_MASK;
-        final int timeoutSec = (msgType == IPCTNL_MSG_CT_NEW) ? 100 /* nonzero, new */
-                : 0 /* unused, delete */;
-        return new ConntrackEvent(
-                (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
-                new Tuple(new TupleIpv4(PRIVATE_ADDR, REMOTE_ADDR),
-                        new TupleProto((byte) proto, PRIVATE_PORT, remotePort)),
-                new Tuple(new TupleIpv4(REMOTE_ADDR, PUBLIC_ADDR),
-                        new TupleProto((byte) proto, remotePort, PUBLIC_PORT)),
-                status,
-                timeoutSec);
-    }
-
-    @NonNull
-    private ConntrackEvent makeTestConntrackEvent(short msgType, int proto) {
-        return makeTestConntrackEvent(msgType, proto, REMOTE_PORT);
-    }
-
-    private void setUpstreamInformationTo(final BpfCoordinator coordinator) {
-        final LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(UPSTREAM_IFACE);
-        lp.addLinkAddress(new LinkAddress(PUBLIC_ADDR, 32 /* prefix length */));
-        final NetworkCapabilities capabilities = new NetworkCapabilities()
-                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
-        coordinator.updateUpstreamNetworkState(new UpstreamNetworkState(lp, capabilities,
-                new Network(TEST_NET_ID)));
-    }
-
-    private void setDownstreamAndClientInformationTo(final BpfCoordinator coordinator) {
-        final ClientInfo clientInfo = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
-                PRIVATE_ADDR, MAC_A /* client mac */);
-        coordinator.tetherOffloadClientAdd(mIpServer, clientInfo);
-    }
-
-    private void initBpfCoordinatorForRule4(final BpfCoordinator coordinator) throws Exception {
-        // Needed because addUpstreamIfindexToMap only updates upstream information when polling
-        // was started.
-        coordinator.startPolling();
-
-        // Needed because two reasons: (1) BpfConntrackEventConsumer#accept only performs cleanup
-        // when both upstream and downstream rules are removed. (2) tetherOffloadRuleRemove of
-        // api31.BpfCoordinatorShimImpl only decreases the count while the entry is deleted.
-        // In the other words, deleteEntry returns true.
-        doReturn(true).when(mBpfUpstream4Map).deleteEntry(any());
-        doReturn(true).when(mBpfDownstream4Map).deleteEntry(any());
+        final UpstreamInformation upstreamInfo = UPSTREAM_INFORMATIONS.get(upstreamIfindex);
+        if (upstreamInfo == null) {
+            fail("Not support upstream interface index " + upstreamIfindex);
+        }
 
         // Needed because BpfCoordinator#addUpstreamIfindexToMap queries interface parameter for
         // interface index.
-        doReturn(UPSTREAM_IFACE_PARAMS).when(mDeps).getInterfaceParams(UPSTREAM_IFACE);
+        doReturn(upstreamInfo.interfaceParams).when(mDeps).getInterfaceParams(
+                upstreamInfo.interfaceParams.name);
+        coordinator.addUpstreamNameToLookupTable(upstreamInfo.interfaceParams.index,
+                upstreamInfo.interfaceParams.name);
 
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
-        setUpstreamInformationTo(coordinator);
-        setDownstreamAndClientInformationTo(coordinator);
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(upstreamInfo.interfaceParams.name);
+        lp.addLinkAddress(new LinkAddress(upstreamInfo.address, 32 /* prefix length */));
+        final NetworkCapabilities capabilities = new NetworkCapabilities()
+                .addTransportType(upstreamInfo.transportType);
+        coordinator.updateUpstreamNetworkState(new UpstreamNetworkState(lp, capabilities,
+                new Network(upstreamInfo.netId)));
+    }
+
+    // Setup downstream interface and its client information to BpfCoordinator.
+    //
+    // @param coordinator BpfCoordinator instance.
+    // @param downstreamIfindex downstream interface index. can be the following values.
+    //        DOWNSTREAM_IFINDEX: a client information which uses MAC_A is added.
+    //        DOWNSTREAM_IFINDEX2: a client information which uses MAC_B is added.
+    // TODO: refactor this function once the client switches between each downstream interface.
+    private void addDownstreamAndClientInformationTo(final BpfCoordinator coordinator,
+            int downstreamIfindex) {
+        if (downstreamIfindex != DOWNSTREAM_IFINDEX && downstreamIfindex != DOWNSTREAM_IFINDEX2) {
+            fail("Not support downstream interface index " + downstreamIfindex);
+        }
+
+        if (downstreamIfindex == DOWNSTREAM_IFINDEX) {
+            coordinator.tetherOffloadClientAdd(mIpServer, CLIENT_INFO_A);
+        } else {
+            coordinator.tetherOffloadClientAdd(mIpServer2, CLIENT_INFO_B);
+        }
+    }
+
+    private void initBpfCoordinatorForRule4(final BpfCoordinator coordinator) throws Exception {
+        setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX);
+        addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX);
     }
 
     // TODO: Test the IPv4 and IPv6 exist concurrently.
@@ -1401,18 +1530,25 @@
         // because the protocol is not an element of the value. Consider using different address
         // or port to make them different for better testing.
         // TODO: Make the values of {TCP, UDP} rules different.
-        final Tether4Key expectedUpstream4KeyTcp = makeUpstream4Key(IPPROTO_TCP);
-        final Tether4Key expectedDownstream4KeyTcp = makeDownstream4Key(IPPROTO_TCP);
-        final Tether4Value expectedUpstream4ValueTcp = makeUpstream4Value();
-        final Tether4Value expectedDownstream4ValueTcp = makeDownstream4Value();
+        final Tether4Key expectedUpstream4KeyTcp = new TestUpstream4Key.Builder()
+                .setProto(IPPROTO_TCP).build();
+        final Tether4Key expectedDownstream4KeyTcp = new TestDownstream4Key.Builder()
+                .setProto(IPPROTO_TCP).build();
+        final Tether4Value expectedUpstream4ValueTcp = new TestUpstream4Value.Builder().build();
+        final Tether4Value expectedDownstream4ValueTcp = new TestDownstream4Value.Builder().build();
 
-        final Tether4Key expectedUpstream4KeyUdp = makeUpstream4Key(IPPROTO_UDP);
-        final Tether4Key expectedDownstream4KeyUdp = makeDownstream4Key(IPPROTO_UDP);
-        final Tether4Value expectedUpstream4ValueUdp = makeUpstream4Value();
-        final Tether4Value expectedDownstream4ValueUdp = makeDownstream4Value();
+        final Tether4Key expectedUpstream4KeyUdp = new TestUpstream4Key.Builder()
+                .setProto(IPPROTO_UDP).build();
+        final Tether4Key expectedDownstream4KeyUdp = new TestDownstream4Key.Builder()
+                .setProto(IPPROTO_UDP).build();
+        final Tether4Value expectedUpstream4ValueUdp = new TestUpstream4Value.Builder().build();
+        final Tether4Value expectedDownstream4ValueUdp = new TestDownstream4Value.Builder().build();
 
         // [1] Adding the first rule on current upstream immediately sends the quota.
-        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP));
+        mConsumer.accept(new TestConntrackEvent.Builder()
+                .setMsgType(IPCTNL_MSG_CT_NEW)
+                .setProto(IPPROTO_TCP)
+                .build());
         verifyTetherOffloadSetInterfaceQuota(inOrder, UPSTREAM_IFINDEX, limit, true /* isInit */);
         inOrder.verify(mBpfUpstream4Map)
                 .insertEntry(eq(expectedUpstream4KeyTcp), eq(expectedUpstream4ValueTcp));
@@ -1421,7 +1557,10 @@
         inOrder.verifyNoMoreInteractions();
 
         // [2] Adding the second rule on current upstream does not send the quota.
-        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
+        mConsumer.accept(new TestConntrackEvent.Builder()
+                .setMsgType(IPCTNL_MSG_CT_NEW)
+                .setProto(IPPROTO_UDP)
+                .build());
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
         inOrder.verify(mBpfUpstream4Map)
                 .insertEntry(eq(expectedUpstream4KeyUdp), eq(expectedUpstream4ValueUdp));
@@ -1430,7 +1569,10 @@
         inOrder.verifyNoMoreInteractions();
 
         // [3] Removing the second rule on current upstream does not send the quota.
-        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_UDP));
+        mConsumer.accept(new TestConntrackEvent.Builder()
+                .setMsgType(IPCTNL_MSG_CT_DELETE)
+                .setProto(IPPROTO_UDP)
+                .build());
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
         inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyUdp));
         inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyUdp));
@@ -1439,7 +1581,10 @@
         // [4] Removing the last rule on current upstream immediately sends the cleanup stuff.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(UPSTREAM_IFINDEX, 0, 0, 0, 0));
-        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_TCP));
+        mConsumer.accept(new TestConntrackEvent.Builder()
+                .setMsgType(IPCTNL_MSG_CT_DELETE)
+                .setProto(IPPROTO_TCP)
+                .build());
         inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyTcp));
         inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyTcp));
         verifyTetherOffloadGetAndClearStats(inOrder, UPSTREAM_IFINDEX);
@@ -1472,14 +1617,20 @@
         final BpfCoordinator coordinator = makeBpfCoordinator();
         initBpfCoordinatorForRule4(coordinator);
 
-        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP));
+        mConsumer.accept(new TestConntrackEvent.Builder()
+                .setMsgType(IPCTNL_MSG_CT_NEW)
+                .setProto(IPPROTO_TCP)
+                .build());
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
                 eq(new TetherDevValue(UPSTREAM_IFINDEX)));
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
                 eq(new TetherDevValue(DOWNSTREAM_IFINDEX)));
         clearInvocations(mBpfDevMap);
 
-        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
+        mConsumer.accept(new TestConntrackEvent.Builder()
+                .setMsgType(IPCTNL_MSG_CT_NEW)
+                .setProto(IPPROTO_UDP)
+                .build());
         verify(mBpfDevMap, never()).updateEntry(any(), any());
     }
 
@@ -1559,10 +1710,10 @@
                 new TestBpfMap<>(Tether4Key.class, Tether4Value.class);
         doReturn(bpfUpstream4Map).when(mDeps).getBpfUpstream4Map();
 
-        final Tether4Key tcpKey = makeUpstream4Key(IPPROTO_TCP);
-        final Tether4Key udpKey = makeUpstream4Key(IPPROTO_UDP);
-        final Tether4Value tcpValue = makeUpstream4Value();
-        final Tether4Value udpValue = makeUpstream4Value();
+        final Tether4Key tcpKey = new TestUpstream4Key.Builder().setProto(IPPROTO_TCP).build();
+        final Tether4Key udpKey = new TestUpstream4Key.Builder().setProto(IPPROTO_UDP).build();
+        final Tether4Value tcpValue = new TestUpstream4Value.Builder().build();
+        final Tether4Value udpValue = new TestUpstream4Value.Builder().build();
 
         checkRefreshConntrackTimeout(bpfUpstream4Map, tcpKey, tcpValue, udpKey, udpValue);
     }
@@ -1575,10 +1726,10 @@
                 new TestBpfMap<>(Tether4Key.class, Tether4Value.class);
         doReturn(bpfDownstream4Map).when(mDeps).getBpfDownstream4Map();
 
-        final Tether4Key tcpKey = makeDownstream4Key(IPPROTO_TCP);
-        final Tether4Key udpKey = makeDownstream4Key(IPPROTO_UDP);
-        final Tether4Value tcpValue = makeDownstream4Value();
-        final Tether4Value udpValue = makeDownstream4Value();
+        final Tether4Key tcpKey = new TestDownstream4Key.Builder().setProto(IPPROTO_TCP).build();
+        final Tether4Key udpKey = new TestDownstream4Key.Builder().setProto(IPPROTO_UDP).build();
+        final Tether4Value tcpValue = new TestDownstream4Value.Builder().build();
+        final Tether4Value udpValue = new TestDownstream4Value.Builder().build();
 
         checkRefreshConntrackTimeout(bpfDownstream4Map, tcpKey, tcpValue, udpKey, udpValue);
     }
@@ -1592,29 +1743,325 @@
         final short offloadedPort = 42;
         assertFalse(CollectionUtils.contains(NON_OFFLOADED_UPSTREAM_IPV4_TCP_PORTS,
                 offloadedPort));
-        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP, offloadedPort));
+        mConsumer.accept(new TestConntrackEvent.Builder()
+                .setMsgType(IPCTNL_MSG_CT_NEW)
+                .setProto(IPPROTO_TCP)
+                .setRemotePort(offloadedPort)
+                .build());
         verify(mBpfUpstream4Map).insertEntry(any(), any());
         verify(mBpfDownstream4Map).insertEntry(any(), any());
         clearInvocations(mBpfUpstream4Map, mBpfDownstream4Map);
 
         for (final short port : NON_OFFLOADED_UPSTREAM_IPV4_TCP_PORTS) {
-            mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP, port));
+            mConsumer.accept(new TestConntrackEvent.Builder()
+                    .setMsgType(IPCTNL_MSG_CT_NEW)
+                    .setProto(IPPROTO_TCP)
+                    .setRemotePort(port)
+                    .build());
             verify(mBpfUpstream4Map, never()).insertEntry(any(), any());
             verify(mBpfDownstream4Map, never()).insertEntry(any(), any());
 
-            mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_TCP, port));
+            mConsumer.accept(new TestConntrackEvent.Builder()
+                    .setMsgType(IPCTNL_MSG_CT_DELETE)
+                    .setProto(IPPROTO_TCP)
+                    .setRemotePort(port)
+                    .build());
             verify(mBpfUpstream4Map, never()).deleteEntry(any());
             verify(mBpfDownstream4Map, never()).deleteEntry(any());
 
-            mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP, port));
+            mConsumer.accept(new TestConntrackEvent.Builder()
+                    .setMsgType(IPCTNL_MSG_CT_NEW)
+                    .setProto(IPPROTO_UDP)
+                    .setRemotePort(port)
+                    .build());
             verify(mBpfUpstream4Map).insertEntry(any(), any());
             verify(mBpfDownstream4Map).insertEntry(any(), any());
             clearInvocations(mBpfUpstream4Map, mBpfDownstream4Map);
 
-            mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_UDP, port));
+            mConsumer.accept(new TestConntrackEvent.Builder()
+                    .setMsgType(IPCTNL_MSG_CT_DELETE)
+                    .setProto(IPPROTO_UDP)
+                    .setRemotePort(port)
+                    .build());
             verify(mBpfUpstream4Map).deleteEntry(any());
             verify(mBpfDownstream4Map).deleteEntry(any());
             clearInvocations(mBpfUpstream4Map, mBpfDownstream4Map);
         }
     }
+
+    // Test network topology:
+    //
+    //            public network                UE                private network
+    //                  |                     /     \                    |
+    // +------------+   V  +-------------+             +--------------+  V  +------------+
+    // |   Sever    +------+  Upstream   |+------+-----+ Downstream 1 +-----+  Client A  |
+    // +------------+      +-------------+|      |     +--------------+     +------------+
+    // remote ip            +-------------+      |                          private ip
+    // 140.112.8.116:443   public ip             |                          192.168.80.12:62449
+    //                     (upstream 1, rawip)   |
+    //                     1.0.0.1:62449         |
+    //                     1.0.0.1:62450         |     +--------------+     +------------+
+    //                            - or -         +-----+ Downstream 2 +-----+  Client B  |
+    //                     (upstream 2, ether)         +--------------+     +------------+
+    //                                                                      private ip
+    //                                                                      192.168.90.12:62450
+    //
+    // Build two test rule sets which include BPF upstream and downstream rules.
+    //
+    // Rule set A: a socket connection from client A to remote server via the first upstream
+    //             (UPSTREAM_IFINDEX).
+    //             192.168.80.12:62449 -> 1.0.0.1:62449 -> 140.112.8.116:443
+    // Rule set B: a socket connection from client B to remote server via the first upstream
+    //             (UPSTREAM_IFINDEX).
+    //             192.168.80.12:62450 -> 1.0.0.1:62450 -> 140.112.8.116:443
+    //
+    // The second upstream (UPSTREAM_IFINDEX2) is an ethernet interface which is not supported by
+    // BPF. Used for testing the rule adding and removing on an unsupported upstream interface.
+    //
+    private static final Tether4Key UPSTREAM4_RULE_KEY_A = makeUpstream4Key(
+            DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, PRIVATE_ADDR, PRIVATE_PORT);
+    private static final Tether4Value UPSTREAM4_RULE_VALUE_A = makeUpstream4Value(PUBLIC_PORT);
+    private static final Tether4Key DOWNSTREAM4_RULE_KEY_A = makeDownstream4Key(PUBLIC_PORT);
+    private static final Tether4Value DOWNSTREAM4_RULE_VALUE_A = makeDownstream4Value(
+            DOWNSTREAM_IFINDEX, MAC_A, DOWNSTREAM_MAC, PRIVATE_ADDR, PRIVATE_PORT);
+
+    private static final Tether4Key UPSTREAM4_RULE_KEY_B = makeUpstream4Key(
+            DOWNSTREAM_IFINDEX2, DOWNSTREAM_MAC2, PRIVATE_ADDR2, PRIVATE_PORT2);
+    private static final Tether4Value UPSTREAM4_RULE_VALUE_B = makeUpstream4Value(PUBLIC_PORT2);
+    private static final Tether4Key DOWNSTREAM4_RULE_KEY_B = makeDownstream4Key(PUBLIC_PORT2);
+    private static final Tether4Value DOWNSTREAM4_RULE_VALUE_B = makeDownstream4Value(
+            DOWNSTREAM_IFINDEX2, MAC_B, DOWNSTREAM_MAC2, PRIVATE_ADDR2, PRIVATE_PORT2);
+
+    private static final ConntrackEvent CONNTRACK_EVENT_A = makeTestConntrackEvent(
+            PUBLIC_PORT, PRIVATE_ADDR, PRIVATE_PORT);
+
+    private static final ConntrackEvent CONNTRACK_EVENT_B = makeTestConntrackEvent(
+            PUBLIC_PORT2, PRIVATE_ADDR2, PRIVATE_PORT2);
+
+    @NonNull
+    private static Tether4Key makeUpstream4Key(final int downstreamIfindex,
+            @NonNull final MacAddress downstreamMac, @NonNull final Inet4Address privateAddr,
+            final short privatePort) {
+        return new Tether4Key(downstreamIfindex, downstreamMac, (short) IPPROTO_TCP,
+            privateAddr.getAddress(), REMOTE_ADDR.getAddress(), privatePort, REMOTE_PORT);
+    }
+
+    @NonNull
+    private static Tether4Key makeDownstream4Key(final short publicPort) {
+        return new Tether4Key(UPSTREAM_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */,
+                (short) IPPROTO_TCP, REMOTE_ADDR.getAddress(), PUBLIC_ADDR.getAddress(),
+                REMOTE_PORT, publicPort);
+    }
+
+    @NonNull
+    private static Tether4Value makeUpstream4Value(final short publicPort) {
+        return new Tether4Value(UPSTREAM_IFINDEX,
+                MacAddress.ALL_ZEROS_ADDRESS /* ethDstMac (rawip) */,
+                MacAddress.ALL_ZEROS_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP,
+                NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(PUBLIC_ADDR),
+                toIpv4MappedAddressBytes(REMOTE_ADDR), publicPort, REMOTE_PORT,
+                0 /* lastUsed */);
+    }
+
+    @NonNull
+    private static Tether4Value makeDownstream4Value(final int downstreamIfindex,
+            @NonNull final MacAddress clientMac, @NonNull final MacAddress downstreamMac,
+            @NonNull final Inet4Address privateAddr, final short privatePort) {
+        return new Tether4Value(downstreamIfindex, clientMac, downstreamMac,
+                ETH_P_IP, NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(REMOTE_ADDR),
+                toIpv4MappedAddressBytes(privateAddr), REMOTE_PORT, privatePort, 0 /* lastUsed */);
+    }
+
+    @NonNull
+    private static ConntrackEvent makeTestConntrackEvent(final short publicPort,
+                @NonNull final Inet4Address privateAddr, final short privatePort) {
+        return new ConntrackEvent(
+                (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | IPCTNL_MSG_CT_NEW),
+                new Tuple(new TupleIpv4(privateAddr, REMOTE_ADDR),
+                        new TupleProto((byte) IPPROTO_TCP, privatePort, REMOTE_PORT)),
+                new Tuple(new TupleIpv4(REMOTE_ADDR, PUBLIC_ADDR),
+                        new TupleProto((byte) IPPROTO_TCP, REMOTE_PORT, publicPort)),
+                ESTABLISHED_MASK,
+                100 /* nonzero, CT_NEW */);
+    }
+
+    void checkRule4ExistInUpstreamDownstreamMap() throws Exception {
+        assertEquals(UPSTREAM4_RULE_VALUE_A, mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A));
+        assertEquals(DOWNSTREAM4_RULE_VALUE_A, mBpfDownstream4Map.getValue(
+                DOWNSTREAM4_RULE_KEY_A));
+        assertEquals(UPSTREAM4_RULE_VALUE_B, mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_B));
+        assertEquals(DOWNSTREAM4_RULE_VALUE_B, mBpfDownstream4Map.getValue(
+                DOWNSTREAM4_RULE_KEY_B));
+    }
+
+    void checkRule4NotExistInUpstreamDownstreamMap() throws Exception {
+        assertNull(mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A));
+        assertNull(mBpfDownstream4Map.getValue(DOWNSTREAM4_RULE_KEY_A));
+        assertNull(mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_B));
+        assertNull(mBpfDownstream4Map.getValue(DOWNSTREAM4_RULE_KEY_B));
+    }
+
+    // Both #addDownstreamAndClientInformationTo and #setUpstreamInformationTo need to be called
+    // before this function because upstream and downstream information are required to build
+    // the rules while conntrack event is received.
+    void addAndCheckRule4ForDownstreams() throws Exception {
+        // Add rule set A which is on the first downstream and rule set B which is on the second
+        // downstream.
+        mConsumer.accept(CONNTRACK_EVENT_A);
+        mConsumer.accept(CONNTRACK_EVENT_B);
+
+        // Check that both rule set A and B were added.
+        checkRule4ExistInUpstreamDownstreamMap();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testTetherOffloadRule4Clear_RemoveDownstream() throws Exception {
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+
+        // Initialize upstream and downstream information manually but calling the setup helper
+        // #initBpfCoordinatorForRule4 because this test needs to {update, remove} upstream and
+        // downstream manually for testing.
+        addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX);
+        addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX2);
+
+        setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX);
+        addAndCheckRule4ForDownstreams();
+
+        // [1] Remove the first downstream. Remove only the rule set A which is on the first
+        // downstream.
+        coordinator.tetherOffloadClientClear(mIpServer);
+        assertNull(mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A));
+        assertNull(mBpfDownstream4Map.getValue(DOWNSTREAM4_RULE_KEY_A));
+        assertEquals(UPSTREAM4_RULE_VALUE_B, mBpfUpstream4Map.getValue(
+                UPSTREAM4_RULE_KEY_B));
+        assertEquals(DOWNSTREAM4_RULE_VALUE_B, mBpfDownstream4Map.getValue(
+                DOWNSTREAM4_RULE_KEY_B));
+
+        // Clear client information for the first downstream only.
+        assertNull(mTetherClients.get(mIpServer));
+        assertNotNull(mTetherClients.get(mIpServer2));
+
+        // [2] Remove the second downstream. Remove the rule set B which is on the second
+        // downstream.
+        coordinator.tetherOffloadClientClear(mIpServer2);
+        assertNull(mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_B));
+        assertNull(mBpfDownstream4Map.getValue(DOWNSTREAM4_RULE_KEY_B));
+
+        // Clear client information for the second downstream.
+        assertNull(mTetherClients.get(mIpServer2));
+    }
+
+    private void asseertClientInfoExist(@NonNull IpServer ipServer,
+            @NonNull ClientInfo clientInfo) {
+        HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+        assertNotNull(clients);
+        assertEquals(clientInfo, clients.get(clientInfo.clientAddress));
+    }
+
+    // Although either ClientInfo for a given downstream (IpServer) is not found or a given
+    // client address is not found on a given downstream can be treated "ClientInfo not
+    // exist", we still want to know the real reason exactly. For example, we don't the
+    // exact reason in the following:
+    //   assertNull(clients == null ? clients : clients.get(clientInfo.clientAddress));
+    // This helper only verifies the case that the downstream still has at least one client.
+    // In other words, ClientInfo for a given IpServer has not been removed yet.
+    private void asseertClientInfoNotExist(@NonNull IpServer ipServer,
+            @NonNull ClientInfo clientInfo) {
+        HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+        assertNotNull(clients);
+        assertNull(clients.get(clientInfo.clientAddress));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testTetherOffloadRule4Clear_ChangeOrRemoveUpstream() throws Exception {
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+
+        // Initialize upstream and downstream information manually but calling the helper
+        // #initBpfCoordinatorForRule4 because this test needs to {update, remove} upstream and
+        // downstream.
+        addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX);
+        addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX2);
+
+        setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX);
+        addAndCheckRule4ForDownstreams();
+
+        // [1] Update the same upstream state. Nothing happens.
+        setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX);
+        checkRule4ExistInUpstreamDownstreamMap();
+
+        // [2] Switch upstream interface from the first upstream (rawip, bpf supported) to
+        // the second upstream (ethernet, bpf not supported). Clear all rules.
+        setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX2);
+        checkRule4NotExistInUpstreamDownstreamMap();
+
+        // Setup the upstream interface information and the rules for next test.
+        setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX);
+        addAndCheckRule4ForDownstreams();
+
+        // [3] Switch upstream from the first upstream (rawip, bpf supported) to no upstream. Clear
+        // all rules.
+        setUpstreamInformationTo(coordinator, INVALID_IFINDEX);
+        checkRule4NotExistInUpstreamDownstreamMap();
+
+        // Client information should be not deleted.
+        asseertClientInfoExist(mIpServer, CLIENT_INFO_A);
+        asseertClientInfoExist(mIpServer2, CLIENT_INFO_B);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testTetherOffloadClientAddRemove() throws Exception {
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+
+        // [1] Add client information A and B on on the same downstream.
+        final ClientInfo clientA = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+                PRIVATE_ADDR, MAC_A);
+        final ClientInfo clientB = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+                PRIVATE_ADDR2, MAC_B);
+        coordinator.tetherOffloadClientAdd(mIpServer, clientA);
+        coordinator.tetherOffloadClientAdd(mIpServer, clientB);
+        asseertClientInfoExist(mIpServer, clientA);
+        asseertClientInfoExist(mIpServer, clientB);
+
+        // Add the rules for client A and client B.
+        final Tether4Key upstream4KeyA = makeUpstream4Key(
+                DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, PRIVATE_ADDR, PRIVATE_PORT);
+        final Tether4Value upstream4ValueA = makeUpstream4Value(PUBLIC_PORT);
+        final Tether4Key downstream4KeyA = makeDownstream4Key(PUBLIC_PORT);
+        final Tether4Value downstream4ValueA = makeDownstream4Value(
+                DOWNSTREAM_IFINDEX, MAC_A, DOWNSTREAM_MAC, PRIVATE_ADDR, PRIVATE_PORT);
+        final Tether4Key upstream4KeyB = makeUpstream4Key(
+                DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC2, PRIVATE_ADDR2, PRIVATE_PORT2);
+        final Tether4Value upstream4ValueB = makeUpstream4Value(PUBLIC_PORT2);
+        final Tether4Key downstream4KeyB = makeDownstream4Key(PUBLIC_PORT2);
+        final Tether4Value downstream4ValueB = makeDownstream4Value(
+                DOWNSTREAM_IFINDEX, MAC_B, DOWNSTREAM_MAC2, PRIVATE_ADDR2, PRIVATE_PORT2);
+
+        mBpfUpstream4Map.insertEntry(upstream4KeyA, upstream4ValueA);
+        mBpfDownstream4Map.insertEntry(downstream4KeyA, downstream4ValueA);
+        mBpfUpstream4Map.insertEntry(upstream4KeyB, upstream4ValueB);
+        mBpfDownstream4Map.insertEntry(downstream4KeyB, downstream4ValueB);
+
+        // [2] Remove client information A. Only the rules on client A should be removed and
+        // the rules on client B should exist.
+        coordinator.tetherOffloadClientRemove(mIpServer, clientA);
+        asseertClientInfoNotExist(mIpServer, clientA);
+        asseertClientInfoExist(mIpServer, clientB);
+        assertNull(mBpfUpstream4Map.getValue(upstream4KeyA));
+        assertNull(mBpfDownstream4Map.getValue(downstream4KeyA));
+        assertEquals(upstream4ValueB, mBpfUpstream4Map.getValue(upstream4KeyB));
+        assertEquals(downstream4ValueB, mBpfDownstream4Map.getValue(downstream4KeyB));
+
+        // [3] Remove client information B. The rules on client B should be removed.
+        // Exactly, ClientInfo for a given IpServer is removed because the last client B
+        // has been removed from the downstream. Can't use the helper #asseertClientInfoExist
+        // to check because the container ClientInfo for a given downstream has been removed.
+        // See #asseertClientInfoExist.
+        coordinator.tetherOffloadClientRemove(mIpServer, clientB);
+        assertNull(mTetherClients.get(mIpServer));
+        assertNull(mBpfUpstream4Map.getValue(upstream4KeyB));
+        assertNull(mBpfDownstream4Map.getValue(downstream4KeyB));
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 46ce82c..690ff71 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -37,6 +37,9 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -47,6 +50,7 @@
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -77,9 +81,12 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.DevSdkIgnoreRule;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
@@ -96,6 +103,7 @@
     private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
     private static final String PROVISIONING_APP_RESPONSE = "app_response";
     private static final String TEST_PACKAGE_NAME = "com.android.tethering.test";
+    private static final String FAILED_TETHERING_REASON = "Tethering provisioning failed.";
     private static final int RECHECK_TIMER_HOURS = 24;
 
     @Mock private CarrierConfigManager mCarrierConfigManager;
@@ -103,10 +111,14 @@
     @Mock private Resources mResources;
     @Mock private SharedLog mLog;
     @Mock private PackageManager mPm;
-    @Mock private EntitlementManager.OnUiEntitlementFailedListener mEntitlementFailedListener;
+    @Mock private EntitlementManager
+            .OnTetherProvisioningFailedListener mTetherProvisioningFailedListener;
     @Mock private AlarmManager mAlarmManager;
     @Mock private PendingIntent mAlarmIntent;
 
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
     // Like so many Android system APIs, these cannot be mocked because it is marked final.
     // We have to use the real versions.
     private final PersistableBundle mCarrierConfig = new PersistableBundle();
@@ -179,8 +191,8 @@
 
         @Override
         protected Intent runSilentTetherProvisioning(int type,
-                final TetheringConfiguration config) {
-            Intent intent = super.runSilentTetherProvisioning(type, config);
+                final TetheringConfiguration config, final ResultReceiver receiver) {
+            Intent intent = super.runSilentTetherProvisioning(type, config, receiver);
             assertSilentTetherProvisioning(type, config, intent);
             silentProvisionCount++;
             addDownstreamMapping(type, fakeEntitlementResult);
@@ -245,7 +257,7 @@
         mPermissionChangeCallback = spy(() -> { });
         mEnMgr = new WrappedEntitlementManager(mMockContext, new Handler(mLooper.getLooper()), mLog,
                 mPermissionChangeCallback);
-        mEnMgr.setOnUiEntitlementFailedListener(mEntitlementFailedListener);
+        mEnMgr.setOnTetherProvisioningFailedListener(mTetherProvisioningFailedListener);
         mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         mEnMgr.setTetheringConfigurationFetcher(() -> {
             return mConfig;
@@ -268,14 +280,23 @@
         when(mResources.getInteger(R.integer.config_mobile_hotspot_provision_check_period))
                 .thenReturn(RECHECK_TIMER_HOURS);
         // Act like the CarrierConfigManager is present and ready unless told otherwise.
-        when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
-                .thenReturn(mCarrierConfigManager);
+        mockService(Context.CARRIER_CONFIG_SERVICE,
+                CarrierConfigManager.class, mCarrierConfigManager);
         when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
         mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
         mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
         mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
     }
 
+    private void setupCarrierConfig(boolean carrierSupported) {
+        mCarrierConfig.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, carrierSupported);
+    }
+
+    private <T> void mockService(String serviceName, Class<T> serviceClass, T service) {
+        when(mMockContext.getSystemServiceName(serviceClass)).thenReturn(serviceName);
+        when(mMockContext.getSystemService(serviceName)).thenReturn(service);
+    }
+
     @Test
     public void canRequireProvisioning() {
         setupForRequiredProvisioning();
@@ -285,8 +306,7 @@
     @Test
     public void toleratesCarrierConfigManagerMissing() {
         setupForRequiredProvisioning();
-        when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
-            .thenReturn(null);
+        mockService(Context.CARRIER_CONFIG_SERVICE, CarrierConfigManager.class, null);
         mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         // Couldn't get the CarrierConfigManager, but still had a declared provisioning app.
         // Therefore provisioning still be required.
@@ -613,14 +633,16 @@
     @Test
     public void testCallStopTetheringWhenUiProvisioningFail() {
         setupForRequiredProvisioning();
-        verify(mEntitlementFailedListener, times(0)).onUiEntitlementFailed(TETHERING_WIFI);
+        verify(mTetherProvisioningFailedListener, times(0))
+                .onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
         mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
         mLooper.dispatchAll();
         assertEquals(1, mEnMgr.uiProvisionCount);
-        verify(mEntitlementFailedListener, times(1)).onUiEntitlementFailed(TETHERING_WIFI);
+        verify(mTetherProvisioningFailedListener, times(1))
+                .onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
     }
 
     @Test
@@ -644,7 +666,8 @@
 
         // When second downstream is down, exempted downstream can use cellular upstream.
         assertEquals(1, mEnMgr.uiProvisionCount);
-        verify(mEntitlementFailedListener).onUiEntitlementFailed(TETHERING_USB);
+        verify(mTetherProvisioningFailedListener).onTetherProvisioningFailed(TETHERING_USB,
+                FAILED_TETHERING_REASON);
         mEnMgr.stopProvisioningIfNeeded(TETHERING_USB);
         assertTrue(mEnMgr.isCellularUpstreamPermitted());
 
@@ -678,4 +701,85 @@
         verify(mAlarmManager).setExact(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), anyLong(),
                 eq(mAlarmIntent));
     }
+
+    @Test
+    @IgnoreUpTo(SC_V2)
+    public void requestLatestTetheringEntitlementResult_carrierDoesNotSupport_noProvisionCount()
+            throws Exception {
+        setupForRequiredProvisioning();
+        setupCarrierConfig(false);
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        ResultReceiver receiver = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
+            }
+        };
+        mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+        mLooper.dispatchAll();
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        mEnMgr.reset();
+    }
+
+    @Test
+    @IgnoreUpTo(SC_V2)
+    public void reevaluateSimCardProvisioning_carrierUnsupportAndSimswitch() {
+        setupForRequiredProvisioning();
+
+        // Start a tethering with cellular data without provisioning.
+        mEnMgr.notifyUpstream(true);
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, false);
+        mLooper.dispatchAll();
+
+        // Tear down mobile, then switch SIM.
+        mEnMgr.notifyUpstream(false);
+        mLooper.dispatchAll();
+        setupCarrierConfig(false);
+        mEnMgr.reevaluateSimCardProvisioning(mConfig);
+
+        // Turn on upstream.
+        mEnMgr.notifyUpstream(true);
+        mLooper.dispatchAll();
+
+        verify(mTetherProvisioningFailedListener)
+                .onTetherProvisioningFailed(TETHERING_WIFI, "Carrier does not support.");
+    }
+
+    @Test
+    @IgnoreUpTo(SC_V2)
+    public void startProvisioningIfNeeded_carrierUnsupport()
+            throws Exception {
+        setupForRequiredProvisioning();
+        setupCarrierConfig(false);
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+        verify(mTetherProvisioningFailedListener, never())
+                .onTetherProvisioningFailed(TETHERING_WIFI, "Carrier does not support.");
+
+        mEnMgr.notifyUpstream(true);
+        mLooper.dispatchAll();
+        verify(mTetherProvisioningFailedListener)
+                .onTetherProvisioningFailed(TETHERING_WIFI, "Carrier does not support.");
+        mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
+        reset(mTetherProvisioningFailedListener);
+
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+        mLooper.dispatchAll();
+        verify(mTetherProvisioningFailedListener)
+                .onTetherProvisioningFailed(TETHERING_WIFI, "Carrier does not support.");
+    }
+
+    @Test
+    public void isTetherProvisioningRequired_carrierUnSupport() {
+        setupForRequiredProvisioning();
+        setupCarrierConfig(false);
+        when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+                .thenReturn(new String[0]);
+        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+        if (SdkLevel.isAtLeastT()) {
+            assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+        } else {
+            assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
+        }
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
index fc34585..e9716b3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -26,7 +26,7 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
 
-import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats;
@@ -666,10 +666,12 @@
         callback.onStoppedLimitReached();
         mTetherStatsProviderCb.expectNotifyStatsUpdated();
 
-        if (isAtLeastS()) {
+        if (isAtLeastT()) {
+            mTetherStatsProviderCb.expectNotifyLimitReached();
+        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
             mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
         } else {
-            mTetherStatsProviderCb.expectLegacyNotifyLimitReached();
+            mTetherStatsProviderCb.expectNotifyLimitReached();
         }
     }
 
@@ -680,7 +682,12 @@
         final OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
         callback.onWarningReached();
         mTetherStatsProviderCb.expectNotifyStatsUpdated();
-        mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
+
+        if (isAtLeastT()) {
+            mTetherStatsProviderCb.expectNotifyWarningReached();
+        } else {
+            mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
+        }
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 6c98f2f..55d9852 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -100,7 +100,6 @@
         when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
         when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
         when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
-        when(mConfig.isSelectAllPrefixRangeEnabled()).thenReturn(true);
         setUpIpServers();
         mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
     }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
index e692015..b2cbf75 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -186,6 +186,16 @@
         makeDefaultNetwork(agent, BROADCAST_FIRST, null /* inBetween */);
     }
 
+    void sendLinkProperties(TestNetworkAgent agent, boolean updateDefaultFirst) {
+        if (!updateDefaultFirst) agent.sendLinkProperties();
+
+        for (NetworkCallback cb : mTrackingDefault.keySet()) {
+            cb.onLinkPropertiesChanged(agent.networkId, agent.linkProperties);
+        }
+
+        if (updateDefaultFirst) agent.sendLinkProperties();
+    }
+
     static boolean looksLikeDefaultRequest(NetworkRequest req) {
         return req.hasCapability(NET_CAPABILITY_INTERNET)
                 && !req.hasCapability(NET_CAPABILITY_DUN)
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index c0c2ab9..7fcf2b2 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -95,7 +95,6 @@
     @Mock private ModuleInfo mMi;
     private Context mMockContext;
     private boolean mHasTelephonyManager;
-    private boolean mEnableLegacyDhcpServer;
     private MockitoSession mMockingSession;
     private MockContentResolver mContentResolver;
 
@@ -182,11 +181,9 @@
         when(mResources.getBoolean(R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip))
                 .thenReturn(false);
         initializeBpfOffloadConfiguration(true, null /* unset */);
-        initEnableSelectAllPrefixRangeFlag(null /* unset */);
 
         mHasTelephonyManager = true;
         mMockContext = new MockContext(mContext);
-        mEnableLegacyDhcpServer = false;
 
         mContentResolver = new MockContentResolver(mMockContext);
         mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
@@ -398,7 +395,7 @@
 
         final TetheringConfiguration enableByRes =
                 new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
-        assertTrue(enableByRes.enableLegacyDhcpServer);
+        assertTrue(enableByRes.useLegacyDhcpServer());
 
         when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
                 false);
@@ -408,7 +405,7 @@
 
         final TetheringConfiguration enableByDevConfig =
                 new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
-        assertTrue(enableByDevConfig.enableLegacyDhcpServer);
+        assertTrue(enableByDevConfig.useLegacyDhcpServer());
     }
 
     @Test
@@ -422,7 +419,7 @@
         final TetheringConfiguration cfg =
                 new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
 
-        assertFalse(cfg.enableLegacyDhcpServer);
+        assertFalse(cfg.useLegacyDhcpServer());
     }
 
     @Test
@@ -490,32 +487,6 @@
         assertTrue(testCfg.shouldEnableWifiP2pDedicatedIp());
     }
 
-    private void initEnableSelectAllPrefixRangeFlag(final String value) {
-        doReturn(value).when(
-                () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
-                eq(TetheringConfiguration.TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES)));
-    }
-
-    @Test
-    public void testSelectAllPrefixRangeFlag() throws Exception {
-        // Test default value.
-        final TetheringConfiguration defaultCfg = new TetheringConfiguration(
-                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
-        assertTrue(defaultCfg.isSelectAllPrefixRangeEnabled());
-
-        // Test disable flag.
-        initEnableSelectAllPrefixRangeFlag("false");
-        final TetheringConfiguration testDisable = new TetheringConfiguration(
-                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
-        assertFalse(testDisable.isSelectAllPrefixRangeEnabled());
-
-        // Test enable flag.
-        initEnableSelectAllPrefixRangeFlag("true");
-        final TetheringConfiguration testEnable = new TetheringConfiguration(
-                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
-        assertTrue(testEnable.isSelectAllPrefixRangeEnabled());
-    }
-
     @Test
     public void testChooseUpstreamAutomatically() throws Exception {
         when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
@@ -645,4 +616,35 @@
         assertArrayEquals(ncmRegexs, cfg.tetherableNcmRegexs);
     }
 
+    @Test
+    public void testP2pLeasesSubnetPrefixLength() throws Exception {
+        when(mResources.getBoolean(R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip))
+                .thenReturn(true);
+
+        final int defaultSubnetPrefixLength = 0;
+        final TetheringConfiguration defaultCfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(defaultSubnetPrefixLength, defaultCfg.getP2pLeasesSubnetPrefixLength());
+
+        final int prefixLengthTooSmall = -1;
+        when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
+                prefixLengthTooSmall);
+        final TetheringConfiguration tooSmallCfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(defaultSubnetPrefixLength, tooSmallCfg.getP2pLeasesSubnetPrefixLength());
+
+        final int prefixLengthTooLarge = 31;
+        when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
+                prefixLengthTooLarge);
+        final TetheringConfiguration tooLargeCfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(defaultSubnetPrefixLength, tooLargeCfg.getP2pLeasesSubnetPrefixLength());
+
+        final int p2pLeasesSubnetPrefixLength = 27;
+        when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
+                p2pLeasesSubnetPrefixLength);
+        final TetheringConfiguration p2pCfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(p2pLeasesSubnetPrefixLength, p2pCfg.getP2pLeasesSubnetPrefixLength());
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index e4dbc7d..0388758 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -152,7 +152,6 @@
 import android.net.ip.IpNeighborMonitor;
 import android.net.ip.IpServer;
 import android.net.ip.RouterAdvertisementDaemon;
-import android.net.util.InterfaceParams;
 import android.net.util.NetworkConstants;
 import android.net.util.SharedLog;
 import android.net.wifi.SoftApConfiguration;
@@ -185,6 +184,7 @@
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.InterfaceParams;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
@@ -297,6 +297,7 @@
     private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
 
     private TestConnectivityManager mCm;
+    private boolean mForceEthernetServiceUnavailable = false;
 
     private class TestContext extends BroadcastInterceptingContext {
         TestContext(Context base) {
@@ -331,7 +332,11 @@
             if (Context.USER_SERVICE.equals(name)) return mUserManager;
             if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager;
             if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm;
-            if (Context.ETHERNET_SERVICE.equals(name)) return mEm;
+            if (Context.ETHERNET_SERVICE.equals(name)) {
+                if (mForceEthernetServiceUnavailable) return null;
+
+                return mEm;
+            }
             return super.getSystemService(name);
         }
 
@@ -452,11 +457,6 @@
         }
 
         @Override
-        public boolean isTetheringSupported() {
-            return true;
-        }
-
-        @Override
         public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
                 int subId) {
             mConfig = spy(new FakeTetheringConfiguration(ctx, log, subId));
@@ -680,6 +680,7 @@
                 .thenReturn(new String[] {TEST_BT_REGEX});
         when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
                 .thenReturn(new String[] {TEST_NCM_REGEX});
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_ETHERNET)).thenReturn(true);
         when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(
                 new int[] {TYPE_WIFI, TYPE_MOBILE_DUN});
         when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
@@ -2834,6 +2835,55 @@
         runDualStackUsbTethering(TEST_RNDIS_IFNAME);
         runStopUSBTethering();
     }
+
+    @Test
+    public void testTetheringSupported() throws Exception {
+        setTetheringSupported(true /* supported */);
+        updateConfigAndVerifySupported(true /* supported */);
+
+        // Could disable tethering supported by settings.
+        Settings.Global.putInt(mContentResolver, Settings.Global.TETHER_SUPPORTED, 0);
+        updateConfigAndVerifySupported(false /* supported */);
+
+        // Could disable tethering supported by user restriction.
+        setTetheringSupported(true /* supported */);
+        when(mUserManager.hasUserRestriction(
+                UserManager.DISALLOW_CONFIG_TETHERING)).thenReturn(true);
+        updateConfigAndVerifySupported(false /* supported */);
+
+        // Tethering is supported if it has any supported downstream.
+        setTetheringSupported(true /* supported */);
+        when(mResources.getStringArray(R.array.config_tether_usb_regexs))
+                .thenReturn(new String[0]);
+        updateConfigAndVerifySupported(true /* supported */);
+        when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
+                .thenReturn(new String[0]);
+        updateConfigAndVerifySupported(true /* supported */);
+
+
+        if (isAtLeastT()) {
+            when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
+                    .thenReturn(new String[0]);
+            updateConfigAndVerifySupported(true /* supported */);
+            when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
+                    .thenReturn(new String[0]);
+            updateConfigAndVerifySupported(true /* supported */);
+            when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
+                    .thenReturn(new String[0]);
+            updateConfigAndVerifySupported(true /* supported */);
+            mForceEthernetServiceUnavailable = true;
+            updateConfigAndVerifySupported(false /* supported */);
+        } else {
+            when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
+                    .thenReturn(new String[0]);
+            updateConfigAndVerifySupported(false /* supported */);
+        }
+    }
+
+    private void updateConfigAndVerifySupported(boolean supported) {
+        sendConfigurationChanged();
+        assertEquals(supported, mTethering.isTetheringSupported());
+    }
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 173679d..97cebd8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -577,6 +577,67 @@
         verify(mEntitleMgr, times(1)).maybeRunProvisioning();
     }
 
+    @Test
+    public void testLinkAddressChanged() {
+        final String ipv4Addr = "100.112.103.18/24";
+        final String ipv6Addr1 = "2001:db8:4:fd00:827a:bfff:fe6f:374d/64";
+        final String ipv6Addr2 = "2003:aa8:3::123/64";
+        mUNM.startTrackDefaultNetwork(mEntitleMgr);
+        mUNM.startObserveAllNetworks();
+        mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
+        mUNM.setTryCell(true);
+
+        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
+        final LinkProperties cellLp = cellAgent.linkProperties;
+        cellLp.setInterfaceName("rmnet0");
+        addLinkAddresses(cellLp, ipv4Addr);
+        cellAgent.fakeConnect();
+        mCM.makeDefaultNetwork(cellAgent);
+        mLooper.dispatchAll();
+        verifyCurrentLinkProperties(cellAgent);
+        int messageIndex = mSM.messages.size() - 1;
+
+        addLinkAddresses(cellLp, ipv6Addr1);
+        mCM.sendLinkProperties(cellAgent, false /* updateDefaultFirst */);
+        mLooper.dispatchAll();
+        verifyCurrentLinkProperties(cellAgent);
+        verifyNotifyLinkPropertiesChange(messageIndex);
+        messageIndex = mSM.messages.size() - 1;
+
+        removeLinkAddresses(cellLp, ipv6Addr1);
+        addLinkAddresses(cellLp, ipv6Addr2);
+        mCM.sendLinkProperties(cellAgent, true /* updateDefaultFirst */);
+        mLooper.dispatchAll();
+        assertEquals(cellAgent.linkProperties, mUNM.getCurrentPreferredUpstream().linkProperties);
+        verifyCurrentLinkProperties(cellAgent);
+        verifyNotifyLinkPropertiesChange(messageIndex);
+    }
+
+    private void verifyCurrentLinkProperties(TestNetworkAgent agent) {
+        assertEquals(agent.networkId, mUNM.getCurrentPreferredUpstream().network);
+        assertEquals(agent.linkProperties, mUNM.getCurrentPreferredUpstream().linkProperties);
+    }
+
+    private void verifyNotifyLinkPropertiesChange(int lastMessageIndex) {
+        assertEquals(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
+                mSM.messages.get(++lastMessageIndex).arg1);
+        assertEquals(UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES,
+                mSM.messages.get(++lastMessageIndex).arg1);
+        assertEquals(lastMessageIndex + 1, mSM.messages.size());
+    }
+
+    private void addLinkAddresses(LinkProperties lp, String... addrs) {
+        for (String addrStr : addrs) {
+            lp.addLinkAddress(new LinkAddress(addrStr));
+        }
+    }
+
+    private void removeLinkAddresses(LinkProperties lp, String... addrs) {
+        for (String addrStr : addrs) {
+            lp.removeLinkAddress(new LinkAddress(addrStr));
+        }
+    }
+
     private void assertSatisfiesLegacyType(int legacyType, UpstreamNetworkState ns) {
         if (legacyType == TYPE_NONE) {
             assertTrue(ns == null);
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 6718402..1fe0e9a 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -47,6 +47,7 @@
         "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
         "//packages/modules/Connectivity/service/native",
+        "//packages/modules/Connectivity/tests/native",
         "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
         "//packages/modules/Connectivity/tests/unit/jni",
         "//system/netd/server",
@@ -58,6 +59,16 @@
 // bpf kernel programs
 //
 bpf {
+    name: "block.o",
+    srcs: ["block.c"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    sub_dir: "net_shared",
+}
+
+bpf {
     name: "dscp_policy.o",
     srcs: ["dscp_policy.c"],
     cflags: [
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
new file mode 100644
index 0000000..ddd9a1c
--- /dev/null
+++ b/bpf_progs/block.c
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <linux/types.h>
+#include <linux/bpf.h>
+#include <netinet/in.h>
+#include <stdint.h>
+
+#include "bpf_helpers.h"
+
+#define ALLOW 1
+#define DISALLOW 0
+
+DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
+        1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
+
+static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
+    if (!ctx->user_port) return ALLOW;
+
+    switch (ctx->protocol) {
+        case IPPROTO_TCP:
+        case IPPROTO_MPTCP:
+        case IPPROTO_UDP:
+        case IPPROTO_UDPLITE:
+        case IPPROTO_DCCP:
+        case IPPROTO_SCTP:
+            break;
+        default:
+            return ALLOW; // unknown protocols are allowed
+    }
+
+    int key = ctx->user_port >> 6;
+    int shift = ctx->user_port & 63;
+
+    uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
+    // Lookup should never fail in reality, but if it does return here to keep the
+    // BPF verifier happy.
+    if (!val) return ALLOW;
+
+    if ((*val >> shift) & 1) return DISALLOW;
+    return ALLOW;
+}
+
+DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM,
+                     bind4_block_port, KVER(5, 4, 0))
+(struct bpf_sock_addr *ctx) {
+    return block_port(ctx);
+}
+
+DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM,
+                     bind6_block_port, KVER(5, 4, 0))
+(struct bpf_sock_addr *ctx) {
+    return block_port(ctx);
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("ConnectivityNative");
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index c798580..e382713 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -65,8 +65,9 @@
            skb->pkt_type == PACKET_MULTICAST;
 }
 
-// try to make the first 'len' header bytes readable via direct packet access
-static inline __always_inline void try_make_readable(struct __sk_buff* skb, int len) {
+// try to make the first 'len' header bytes readable/writable via direct packet access
+// (note: AFAIK there is no way to ask for only direct packet read without also getting write)
+static inline __always_inline void try_make_writable(struct __sk_buff* skb, int len) {
     if (len > skb->len) len = skb->len;
     if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
 }
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index dc646c3..9a9d337 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -37,21 +37,33 @@
 // From kernel:include/net/ip.h
 #define IP_DF 0x4000  // Flag: "Don't Fragment"
 
+// Used for iptables drops ingress clat packet. Beware of clat mark change may break the device
+// which is using the old clat mark in netd platform code. The reason is that the clat mark is a
+// mainline constant since T+ but netd iptable rules (ex: bandwidth control, firewall, and so on)
+// are set in stone.
+#define CLAT_MARK 0xdeadc1a7
+
 DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
 
 static inline __always_inline int nat64(struct __sk_buff* skb, bool is_ethernet) {
-    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
-    void* data = (void*)(long)skb->data;
-    const void* data_end = (void*)(long)skb->data_end;
-    const struct ethhdr* const eth = is_ethernet ? data : NULL;  // used iff is_ethernet
-    const struct ipv6hdr* const ip6 = is_ethernet ? (void*)(eth + 1) : data;
-
     // Require ethernet dst mac address to be our unicast address.
     if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
 
     // Must be meta-ethernet IPv6 frame
     if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
 
+    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+    // Not clear if this is actually necessary considering we use DPA (Direct Packet Access),
+    // but we need to make sure we can read the IPv6 header reliably so that we can set
+    // skb->mark = 0xDeadC1a7 for packets we fail to offload.
+    try_make_writable(skb, l2_header_size + sizeof(struct ipv6hdr));
+
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+    const struct ethhdr* const eth = is_ethernet ? data : NULL;  // used iff is_ethernet
+    const struct ipv6hdr* const ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
     // Must have (ethernet and) ipv6 header
     if (data + l2_header_size + sizeof(*ip6) > data_end) return TC_ACT_PIPE;
 
@@ -64,17 +76,6 @@
     // Maximum IPv6 payload length that can be translated to IPv4
     if (ntohs(ip6->payload_len) > 0xFFFF - sizeof(struct iphdr)) return TC_ACT_PIPE;
 
-    switch (ip6->nexthdr) {
-        case IPPROTO_TCP:  // For TCP & UDP the checksum neutrality of the chosen IPv6
-        case IPPROTO_UDP:  // address means there is no need to update their checksums.
-        case IPPROTO_GRE:  // We do not need to bother looking at GRE/ESP headers,
-        case IPPROTO_ESP:  // since there is never a checksum to update.
-            break;
-
-        default:  // do not know how to handle anything else
-            return TC_ACT_PIPE;
-    }
-
     ClatIngress6Key k = {
             .iif = skb->ifindex,
             .pfx96.in6_u.u6_addr32 =
@@ -90,6 +91,21 @@
 
     if (!v) return TC_ACT_PIPE;
 
+    switch (ip6->nexthdr) {
+        case IPPROTO_TCP:  // For TCP & UDP the checksum neutrality of the chosen IPv6
+        case IPPROTO_UDP:  // address means there is no need to update their checksums.
+        case IPPROTO_GRE:  // We do not need to bother looking at GRE/ESP headers,
+        case IPPROTO_ESP:  // since there is never a checksum to update.
+            break;
+
+        default:  // do not know how to handle anything else
+            // Mark ingress non-offloaded clat packet for dropping in ip6tables bw_raw_PREROUTING.
+            // Non-offloaded clat packet is going to be handled by clat daemon and ip6tables. The
+            // duplicate one in ip6tables is not necessary.
+            skb->mark = CLAT_MARK;
+            return TC_ACT_PIPE;
+    }
+
     struct ethhdr eth2;  // used iff is_ethernet
     if (is_ethernet) {
         eth2 = *eth;                     // Copy over the ethernet header (src/dst mac)
@@ -132,7 +148,13 @@
 
     // Packet mutations begin - point of no return, but if this first modification fails
     // the packet is probably still pristine, so let clatd handle it.
-    if (bpf_skb_change_proto(skb, htons(ETH_P_IP), 0)) return TC_ACT_PIPE;
+    if (bpf_skb_change_proto(skb, htons(ETH_P_IP), 0)) {
+        // Mark ingress non-offloaded clat packet for dropping in ip6tables bw_raw_PREROUTING.
+        // Non-offloaded clat packet is going to be handled by clat daemon and ip6tables. The
+        // duplicate one in ip6tables is not necessary.
+        skb->mark = CLAT_MARK;
+        return TC_ACT_PIPE;
+    }
 
     // This takes care of updating the skb->csum field for a CHECKSUM_COMPLETE packet.
     //
@@ -198,13 +220,16 @@
 
 DEFINE_BPF_PROG("schedcls/egress4/clat_rawip", AID_ROOT, AID_SYSTEM, sched_cls_egress4_clat_rawip)
 (struct __sk_buff* skb) {
+    // Must be meta-ethernet IPv4 frame
+    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
+
+    // Possibly not needed, but for consistency with nat64 up above
+    try_make_writable(skb, sizeof(struct iphdr));
+
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
     const struct iphdr* const ip4 = data;
 
-    // Must be meta-ethernet IPv4 frame
-    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
-
     // Must have ipv4 header
     if (data + sizeof(*ip4) > data_end) return TC_ACT_PIPE;
 
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c
index 9989e6b..d5df7ef 100644
--- a/bpf_progs/dscp_policy.c
+++ b/bpf_progs/dscp_policy.c
@@ -16,6 +16,7 @@
 
 #include <linux/types.h>
 #include <linux/bpf.h>
+#include <linux/if_packet.h>
 #include <linux/ip.h>
 #include <linux/ipv6.h>
 #include <linux/if_ether.h>
@@ -27,249 +28,294 @@
 #include <string.h>
 
 #include "bpf_helpers.h"
+#include "dscp_policy.h"
 
-#define MAX_POLICIES 16
-#define MAP_A 1
-#define MAP_B 2
-
-#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
-
-// TODO: these are already defined in /system/netd/bpf_progs/bpf_net_helpers.h
-// should they be moved to common location?
-static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
-        (void*)BPF_FUNC_get_socket_cookie;
-static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
-                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
-static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
-                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
-
-typedef struct {
-    // Add family here to match __sk_buff ?
-    struct in_addr srcIp;
-    struct in_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPort;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t pad[2];
-} Ipv4RuleEntry;
-STRUCT_SIZE(Ipv4RuleEntry, 2 * 4 + 2 * 2 + 2 * 1 + 2);  // 16, 4 for in_addr
-
-#define SRC_IP_MASK     1
-#define DST_IP_MASK     2
-#define SRC_PORT_MASK   4
-#define DST_PORT_MASK   8
-#define PROTO_MASK      16
-
-typedef struct {
-    struct in6_addr srcIp;
-    struct in6_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPortStart;
-    __be16 dstPortEnd;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t mask;
-    uint8_t pad[3];
-} Ipv4Policy;
-STRUCT_SIZE(Ipv4Policy, 2 * 16 + 3 * 2 + 3 * 1 + 3);  // 44
-
-typedef struct {
-    struct in6_addr srcIp;
-    struct in6_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPortStart;
-    __be16 dstPortEnd;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t mask;
-    // should we override this struct to include the param bitmask for linear search?
-    // For mapping socket to policies, all the params should match exactly since we can
-    // pull any missing from the sock itself.
-} Ipv6RuleEntry;
-STRUCT_SIZE(Ipv6RuleEntry, 2 * 16 + 3 * 2 + 3 * 1 + 3);  // 44
-
-// TODO: move to using 1 map. Map v4 address to 0xffff::v4
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
 DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM)
 
-DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, Ipv4Policy, MAX_POLICIES,
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
         AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, Ipv6RuleEntry, MAX_POLICIES,
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
         AID_SYSTEM)
 
-DEFINE_BPF_PROG_KVER("schedcls/set_dscp", AID_ROOT, AID_SYSTEM,
-                     schedcls_set_dscp, KVER(5, 4, 0))
-(struct __sk_buff* skb) {
-    int one = 0;
-    uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&one);
+DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES,
+        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES,
+        AID_SYSTEM)
+
+static inline __always_inline void match_policy(struct __sk_buff* skb, bool ipv4, bool is_eth) {
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+
+    const int l2_header_size = is_eth ? sizeof(struct ethhdr) : 0;
+    struct ethhdr* eth = is_eth ? data : NULL;
+
+    if (data + l2_header_size > data_end) return;
+
+    int zero = 0;
+    int hdr_size = 0;
+    uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&zero);
 
     // use this with HASH map so map lookup only happens once policies have been added?
     if (!selectedMap) {
-        return TC_ACT_PIPE;
+        return;
     }
 
     // used for map lookup
     uint64_t cookie = bpf_get_socket_cookie(skb);
+    if (!cookie)
+        return;
 
-    // Do we need separate maps for ipv4/ipv6
-    if (skb->protocol == htons(ETH_P_IP)) { //maybe bpf_htons()
-        Ipv4RuleEntry* v4Policy;
-        if (*selectedMap == MAP_A) {
-            v4Policy = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
-        } else {
-            v4Policy = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
-        }
-
-        // How to use bitmask here to compare params efficiently?
-        // TODO: add BPF_PROG_TYPE_SK_SKB prog type to Loader?
-
-        void* data = (void*)(long)skb->data;
-        const void* data_end = (void*)(long)skb->data_end;
-        const struct iphdr* const iph = data;
-
+    uint16_t sport = 0;
+    uint16_t dport = 0;
+    uint8_t protocol = 0; // TODO: Use are reserved value? Or int (-1) and cast to uint below?
+    struct in6_addr srcIp = {};
+    struct in6_addr dstIp = {};
+    uint8_t tos = 0; // Only used for IPv4
+    uint8_t priority = 0; // Only used for IPv6
+    uint8_t flow_lbl = 0; // Only used for IPv6
+    if (ipv4) {
+        const struct iphdr* const iph = is_eth ? (void*)(eth + 1) : data;
         // Must have ipv4 header
-        if (data + sizeof(*iph) > data_end) return TC_ACT_PIPE;
+        if (data + l2_header_size + sizeof(*iph) > data_end) return;
 
         // IP version must be 4
-        if (iph->version != 4) return TC_ACT_PIPE;
+        if (iph->version != 4) return;
 
         // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
-        if (iph->ihl != 5) return TC_ACT_PIPE;
+        if (iph->ihl != 5) return;
 
-        if (iph->protocol != IPPROTO_UDP) return TC_ACT_PIPE;
+        // V4 mapped address in in6_addr sets 10/11 position to 0xff.
+        srcIp.s6_addr32[2] = htonl(0x0000ffff);
+        dstIp.s6_addr32[2] = htonl(0x0000ffff);
 
-        struct udphdr *udp;
-        udp = data + sizeof(struct iphdr); //sizeof(struct ethhdr)
+        // Copy IPv4 address into in6_addr for easy comparison below.
+        srcIp.s6_addr32[3] = iph->saddr;
+        dstIp.s6_addr32[3] = iph->daddr;
+        protocol = iph->protocol;
+        tos = iph->tos;
+        hdr_size = sizeof(struct iphdr);
+    } else {
+        struct ipv6hdr* ip6h = is_eth ? (void*)(eth + 1) : data;
+        // Must have ipv6 header
+        if (data + l2_header_size + sizeof(*ip6h) > data_end) return;
 
-        if ((void*)(udp + 1) > data_end) return TC_ACT_PIPE;
+        if (ip6h->version != 6) return;
 
-        // Source/destination port in udphdr are stored in be16, need to convert to le16.
-        // This can be done via ntohs or htons. Is there a more preferred way?
-        // Cached policy was found.
-        if (v4Policy && iph->saddr == v4Policy->srcIp.s_addr &&
-                    iph->daddr == v4Policy->dstIp.s_addr &&
-                    ntohs(udp->source) == v4Policy->srcPort &&
-                    ntohs(udp->dest) == v4Policy->dstPort &&
-                    iph->protocol == v4Policy->proto) {
-            // set dscpVal in packet. Least sig 2 bits of TOS
-            // reference ipv4_change_dsfield()
+        srcIp = ip6h->saddr;
+        dstIp = ip6h->daddr;
+        protocol = ip6h->nexthdr;
+        priority = ip6h->priority;
+        flow_lbl = ip6h->flow_lbl[0];
+        hdr_size = sizeof(struct ipv6hdr);
+    }
 
-            // TODO: fix checksum...
-            int ecn = iph->tos & 3;
-            uint8_t newDscpVal = (v4Policy->dscpVal << 2) + ecn;
-            int oldDscpVal = iph->tos >> 2;
+    switch (protocol) {
+        case IPPROTO_UDP:
+        case IPPROTO_UDPLITE:
+        {
+            struct udphdr *udp;
+            udp = data + hdr_size;
+            if ((void*)(udp + 1) > data_end) return;
+            sport = udp->source;
+            dport = udp->dest;
+        }
+        break;
+        case IPPROTO_TCP:
+        {
+            struct tcphdr *tcp;
+            tcp = data + hdr_size;
+            if ((void*)(tcp + 1) > data_end) return;
+            sport = tcp->source;
+            dport = tcp->dest;
+        }
+        break;
+        default:
+            return;
+    }
+
+    RuleEntry* existingRule;
+    if (ipv4) {
+        if (*selectedMap == MAP_A) {
+            existingRule = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
+        } else {
+            existingRule = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
+        }
+    } else {
+        if (*selectedMap == MAP_A) {
+            existingRule = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+        } else {
+            existingRule = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+        }
+    }
+
+    if (existingRule && v6_equal(srcIp, existingRule->srcIp) &&
+                v6_equal(dstIp, existingRule->dstIp) &&
+                skb->ifindex == existingRule->ifindex &&
+                ntohs(sport) == htons(existingRule->srcPort) &&
+                ntohs(dport) == htons(existingRule->dstPort) &&
+                protocol == existingRule->proto) {
+        if (ipv4) {
+            int ecn = tos & 3;
+            uint8_t newDscpVal = (existingRule->dscpVal << 2) + ecn;
+            int oldDscpVal = tos >> 2;
             bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
             bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
-            return TC_ACT_PIPE;
+        } else {
+            uint8_t new_priority = (existingRule->dscpVal >> 2) + 0x60;
+            uint8_t new_flow_label = ((existingRule->dscpVal & 0xf) << 6) + (priority >> 6);
+            bpf_skb_store_bytes(skb, 0, &new_priority, sizeof(uint8_t), 0);
+            bpf_skb_store_bytes(skb, 1, &new_flow_label, sizeof(uint8_t), 0);
+        }
+        return;
+    }
+
+    // Linear scan ipv4_dscp_policies_map since no stored params match skb.
+    int bestScore = -1;
+    uint32_t bestMatch = 0;
+
+    for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
+        int score = 0;
+        uint8_t tempMask = 0;
+        // Using a uint64 in for loop prevents infinite loop during BPF load,
+        // but the key is uint32, so convert back.
+        uint32_t key = i;
+
+        DscpPolicy* policy;
+        if (ipv4) {
+            policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+        } else {
+            policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
         }
 
-        // linear scan ipv4_dscp_policies_map, stored socket params do not match actual
-        int bestScore = -1;
-        uint32_t bestMatch = 0;
+        // If the policy lookup failed, presentFields is 0, or iface index does not match
+        // index on skb buff, then we can continue to next policy.
+        if (!policy || policy->presentFields == 0 || policy->ifindex != skb->ifindex)
+            continue;
 
-        for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
-            int score = 0;
-            uint8_t tempMask = 0;
-            // Using a uint62 in for loop prevents infinite loop during BPF load,
-            // but the key is uint32, so convert back.
-            uint32_t key = i;
-            Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+        if ((policy->presentFields & SRC_IP_MASK_FLAG) == SRC_IP_MASK_FLAG &&
+                v6_equal(srcIp, policy->srcIp)) {
+            score++;
+            tempMask |= SRC_IP_MASK_FLAG;
+        }
+        if ((policy->presentFields & DST_IP_MASK_FLAG) == DST_IP_MASK_FLAG &&
+                v6_equal(dstIp, policy->dstIp)) {
+            score++;
+            tempMask |= DST_IP_MASK_FLAG;
+        }
+        if ((policy->presentFields & SRC_PORT_MASK_FLAG) == SRC_PORT_MASK_FLAG &&
+                ntohs(sport) == htons(policy->srcPort)) {
+            score++;
+            tempMask |= SRC_PORT_MASK_FLAG;
+        }
+        if ((policy->presentFields & DST_PORT_MASK_FLAG) == DST_PORT_MASK_FLAG &&
+                ntohs(dport) >= htons(policy->dstPortStart) &&
+                ntohs(dport) <= htons(policy->dstPortEnd)) {
+            score++;
+            tempMask |= DST_PORT_MASK_FLAG;
+        }
+        if ((policy->presentFields & PROTO_MASK_FLAG) == PROTO_MASK_FLAG &&
+                protocol == policy->proto) {
+            score++;
+            tempMask |= PROTO_MASK_FLAG;
+        }
 
-            // if mask is 0 continue, key does not have corresponding policy value
-            if (policy && policy->mask != 0) {
-                if ((policy->mask & SRC_IP_MASK) == SRC_IP_MASK &&
-                        iph->saddr == policy->srcIp.s6_addr32[3]) {
-                    score++;
-                    tempMask |= SRC_IP_MASK;
-                }
-                if ((policy->mask & DST_IP_MASK) == DST_IP_MASK &&
-                        iph->daddr == policy->dstIp.s6_addr32[3]) {
-                    score++;
-                    tempMask |= DST_IP_MASK;
-                }
-                if ((policy->mask & SRC_PORT_MASK) == SRC_PORT_MASK &&
-                        ntohs(udp->source) == htons(policy->srcPort)) {
-                    score++;
-                    tempMask |= SRC_PORT_MASK;
-                }
-                if ((policy->mask & DST_PORT_MASK) == DST_PORT_MASK &&
-                        ntohs(udp->dest) >= htons(policy->dstPortStart) &&
-                        ntohs(udp->dest) <= htons(policy->dstPortEnd)) {
-                    score++;
-                    tempMask |= DST_PORT_MASK;
-                }
-                if ((policy->mask & PROTO_MASK) == PROTO_MASK &&
-                        iph->protocol == policy->proto) {
-                    score++;
-                    tempMask |= PROTO_MASK;
-                }
+        if (score > bestScore && tempMask == policy->presentFields) {
+            bestMatch = i;
+            bestScore = score;
+        }
+    }
 
-                if (score > bestScore && tempMask == policy->mask) {
-                    bestMatch = i;
-                    bestScore = score;
-                }
+    uint8_t new_tos= 0; // Can 0 be used as default forwarding value?
+    uint8_t new_priority = 0;
+    uint8_t new_flow_lbl = 0;
+    if (bestScore > 0) {
+        DscpPolicy* policy;
+        if (ipv4) {
+            policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
+        } else {
+            policy = bpf_ipv6_dscp_policies_map_lookup_elem(&bestMatch);
+        }
+
+        if (policy) {
+            // TODO: if DSCP value is already set ignore?
+            if (ipv4) {
+                int ecn = tos & 3;
+                new_tos = (policy->dscpVal << 2) + ecn;
+            } else {
+                new_priority = (policy->dscpVal >> 2) + 0x60;
+                new_flow_lbl = ((policy->dscpVal & 0xf) << 6) + (flow_lbl >> 6);
+
+                // Set IPv6 curDscp value to stored value and recalulate priority
+                // and flow label during next use.
+                new_tos = policy->dscpVal;
             }
         }
+    } else return;
 
-        uint8_t newDscpVal = 0; // Can 0 be used as default forwarding value?
-        uint8_t curDscp = iph->tos & 252;
-        if (bestScore > 0) {
-            Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
-            if (policy) {
-                // TODO: if DSCP value is already set ignore?
-                // TODO: update checksum, for testing increment counter...
-                int ecn = iph->tos & 3;
-                newDscpVal = (policy->dscpVal << 2) + ecn;
-            }
-        }
+    RuleEntry value = {
+        .srcIp = srcIp,
+        .dstIp = dstIp,
+        .ifindex = skb->ifindex,
+        .srcPort = sport,
+        .dstPort = dport,
+        .proto = protocol,
+        .dscpVal = new_tos,
+    };
 
-        Ipv4RuleEntry value = {
-            .srcIp.s_addr = iph->saddr,
-            .dstIp.s_addr = iph->daddr,
-            .srcPort = udp->source,
-            .dstPort = udp->dest,
-            .proto = iph->protocol,
-            .dscpVal = newDscpVal,
-        };
-
-        if (!cookie)
-            return TC_ACT_PIPE;
-
-        // Update map
+    //Update map with new policy.
+    if (ipv4) {
         if (*selectedMap == MAP_A) {
             bpf_ipv4_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
         } else {
             bpf_ipv4_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
         }
-
-        // Need to store bytes after updating map or program will not load.
-        if (newDscpVal != curDscp) {
-            // 1 is the offset (Version/Header length)
-            int oldDscpVal = iph->tos >> 2;
-            bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
-            bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
-        }
-
-    } else if (skb->protocol == htons(ETH_P_IPV6)) { //maybe bpf_htons()
-        Ipv6RuleEntry* v6Policy;
+    } else {
         if (*selectedMap == MAP_A) {
-            v6Policy = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+            bpf_ipv6_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
         } else {
-            v6Policy = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+            bpf_ipv6_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
         }
+    }
 
-        if (!v6Policy)
-            return TC_ACT_PIPE;
+    // Need to store bytes after updating map or program will not load.
+    if (ipv4 && new_tos != (tos & 252)) {
+        int oldDscpVal = tos >> 2;
+        bpf_l3_csum_replace(skb, 1, oldDscpVal, new_tos, sizeof(uint8_t));
+        bpf_skb_store_bytes(skb, 1, &new_tos, sizeof(uint8_t), 0);
+    } else if (!ipv4 && (new_priority != priority || new_flow_lbl != flow_lbl)) {
+        bpf_skb_store_bytes(skb, 0, &new_priority, sizeof(uint8_t), 0);
+        bpf_skb_store_bytes(skb, 1, &new_flow_lbl, sizeof(uint8_t), 0);
+    }
+    return;
+}
 
-        // TODO: Add code to process IPv6 packet.
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM,
+                     schedcls_set_dscp_ether, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+
+    if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
+
+    if (skb->protocol == htons(ETH_P_IP)) {
+        match_policy(skb, true, true);
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        match_policy(skb, false, true);
+    }
+
+    // Always return TC_ACT_PIPE
+    return TC_ACT_PIPE;
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_raw_ip", AID_ROOT, AID_SYSTEM,
+                     schedcls_set_dscp_raw_ip, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+    if (skb->protocol == htons(ETH_P_IP)) {
+        match_policy(skb, true, false);
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        match_policy(skb, false, false);
     }
 
     // Always return TC_ACT_PIPE
diff --git a/bpf_progs/dscp_policy.h b/bpf_progs/dscp_policy.h
new file mode 100644
index 0000000..777c4ff
--- /dev/null
+++ b/bpf_progs/dscp_policy.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define MAX_POLICIES 16
+#define MAP_A 1
+#define MAP_B 2
+
+#define SRC_IP_MASK_FLAG     1
+#define DST_IP_MASK_FLAG     2
+#define SRC_PORT_MASK_FLAG   4
+#define DST_PORT_MASK_FLAG   8
+#define PROTO_MASK_FLAG      16
+
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+#ifndef v6_equal
+#define v6_equal(a, b)    (a.s6_addr32[0] == b.s6_addr32[0] && \
+                 a.s6_addr32[1] == b.s6_addr32[1] && \
+                 a.s6_addr32[2] == b.s6_addr32[2] && \
+                 a.s6_addr32[3] == b.s6_addr32[3])
+#endif
+
+// TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
+// smove to common location in future.
+static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
+        (void*)BPF_FUNC_get_socket_cookie;
+static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
+static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
+static long (*bpf_skb_ecn_set_ce)(struct __sk_buff* skb) =
+        (void*)BPF_FUNC_skb_ecn_set_ce;
+
+typedef struct {
+    struct in6_addr srcIp;
+    struct in6_addr dstIp;
+    uint32_t ifindex;
+    __be16 srcPort;
+    __be16 dstPortStart;
+    __be16 dstPortEnd;
+    uint8_t proto;
+    uint8_t dscpVal;
+    uint8_t presentFields;
+    uint8_t pad[3];
+} DscpPolicy;
+STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 3 * 1 + 3);  // 48
+
+typedef struct {
+    struct in6_addr srcIp;
+    struct in6_addr dstIp;
+    __u32 ifindex;
+    __be16 srcPort;
+    __be16 dstPort;
+    __u8 proto;
+    __u8 dscpVal;
+    __u8 pad[2];
+} RuleEntry;
+STRUCT_SIZE(RuleEntry, 2 * 16 + 1 * 4 + 2 * 2 + 2 * 1 + 2);  // 44
\ No newline at end of file
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 8d05757..fe9a871 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -340,9 +340,11 @@
 
 DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog)
 (struct __sk_buff* skb) {
-    // Account for ingress traffic before tc drops it.
-    uint32_t key = skb->ifindex;
-    update_iface_stats_map(skb, BPF_INGRESS, &key);
+    if (is_received_skb(skb)) {
+        // Account for ingress traffic before tc drops it.
+        uint32_t key = skb->ifindex;
+        update_iface_stats_map(skb, BPF_INGRESS, &key);
+    }
     return TC_ACT_UNSPEC;
 }
 
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 977e918..92a774c 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -122,7 +122,7 @@
     // not trigger and thus we need to manually make sure we can read packet headers via DPA.
     // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
     // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
-    try_make_readable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+    try_make_writable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
 
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
@@ -369,7 +369,7 @@
     // not trigger and thus we need to manually make sure we can read packet headers via DPA.
     // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
     // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
-    try_make_readable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
+    try_make_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
 
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index a76e346..c9c73f1 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -28,6 +28,9 @@
 // Used only by TetheringPrivilegedTests, not by production code.
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
                    AID_NETWORK_STACK)
+// Used only by BpfBitmapTest, not by production code.
+DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2,
+                   AID_NETWORK_STACK)
 
 DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK,
                       xdp_test, KVER(5, 9, 0))
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 50a3eb2..1e508a0 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -19,6 +19,9 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// Include build rules from Sources.bp
+build = ["Sources.bp"]
+
 java_defaults {
     name: "enable-framework-connectivity-t-targets",
     enabled: true,
@@ -30,7 +33,7 @@
 // T, and were moved to the module in T. Other bootclasspath classes in connectivity should go to
 // framework-connectivity.
 java_defaults {
-    name: "framework-connectivity-tiramisu-defaults",
+    name: "framework-connectivity-t-defaults",
     sdk_version: "module_current",
     min_sdk_version: "Tiramisu",
     defaults: [
@@ -53,6 +56,7 @@
         // The build system will use framework-bluetooth module_current stubs, because
         // of sdk_version: "module_current" above.
         "framework-bluetooth",
+        "framework-wifi",
         // Compile against the entire implementation of framework-connectivity,
         // including hidden methods. This is safe because if framework-connectivity-t is
         // on the bootclasspath (i.e., T), then framework-connectivity is also on the
@@ -77,10 +81,11 @@
 }
 
 java_library {
-    name: "framework-connectivity-tiramisu-pre-jarjar",
-    defaults: ["framework-connectivity-tiramisu-defaults"],
+    name: "framework-connectivity-t-pre-jarjar",
+    defaults: ["framework-connectivity-t-defaults"],
     libs: [
         "framework-bluetooth",
+        "framework-wifi",
         "framework-connectivity-pre-jarjar",
     ],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
@@ -90,9 +95,9 @@
 // T, and were moved to the module in T. Other bootclasspath classes in connectivity should go to
 // framework-connectivity.
 java_sdk_library {
-    name: "framework-connectivity-tiramisu",
+    name: "framework-connectivity-t",
     defaults: [
-        "framework-connectivity-tiramisu-defaults",
+        "framework-connectivity-t-defaults",
         "enable-framework-connectivity-t-targets",
     ],
     // Do not add static_libs to this library: put them in framework-connectivity instead.
@@ -100,6 +105,7 @@
     // framework-connectivity-pre-jarjar match at runtime.
     jarjar_rules: ":connectivity-jarjar-rules",
     permitted_packages: [
+        "android.app.usage",
         "android.net",
         "android.net.nsd",
         "android.nearby",
@@ -124,6 +130,7 @@
         "//frameworks/base/tests/vcn",
         "//frameworks/libs/net/common/testutils",
         "//frameworks/libs/net/common/tests:__subpackages__",
+        "//frameworks/opt/net/ethernet/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
         "//packages/modules/CaptivePortalLogin/tests",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
diff --git a/framework-t/Sources.bp b/framework-t/Sources.bp
new file mode 100644
index 0000000..b30ee80
--- /dev/null
+++ b/framework-t/Sources.bp
@@ -0,0 +1,168 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+// NetworkStats related libraries.
+
+filegroup {
+    name: "framework-connectivity-netstats-internal-sources",
+    srcs: [
+        "src/android/app/usage/*.java",
+        "src/android/net/DataUsageRequest.*",
+        "src/android/net/INetworkStatsService.aidl",
+        "src/android/net/INetworkStatsSession.aidl",
+        "src/android/net/NetworkIdentity.java",
+        "src/android/net/NetworkIdentitySet.java",
+        "src/android/net/NetworkStateSnapshot.*",
+        "src/android/net/NetworkStats.*",
+        "src/android/net/NetworkStatsAccess.*",
+        "src/android/net/NetworkStatsCollection.*",
+        "src/android/net/NetworkStatsHistory.*",
+        "src/android/net/NetworkTemplate.*",
+        "src/android/net/TrafficStats.java",
+        "src/android/net/UnderlyingNetworkInfo.*",
+        "src/android/net/netstats/**/*.*",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+filegroup {
+    name: "framework-connectivity-netstats-sources",
+    srcs: [
+        ":framework-connectivity-netstats-internal-sources",
+    ],
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// Nsd related libraries.
+
+filegroup {
+    name: "framework-connectivity-nsd-internal-sources",
+    srcs: [
+        "src/android/net/nsd/*.aidl",
+        "src/android/net/nsd/*.java",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+filegroup {
+    name: "framework-connectivity-nsd-sources",
+    srcs: [
+        ":framework-connectivity-nsd-internal-sources",
+    ],
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// IpSec related libraries.
+
+filegroup {
+    name: "framework-connectivity-ipsec-sources",
+    srcs: [
+        "src/android/net/IIpSecService.aidl",
+        "src/android/net/IpSec*.*",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// Ethernet related libraries.
+
+filegroup {
+    name: "framework-connectivity-ethernet-sources",
+    srcs: [
+        "src/android/net/EthernetManager.java",
+        "src/android/net/EthernetNetworkManagementException.java",
+        "src/android/net/EthernetNetworkManagementException.aidl",
+        "src/android/net/EthernetNetworkSpecifier.java",
+        "src/android/net/EthernetNetworkUpdateRequest.java",
+        "src/android/net/EthernetNetworkUpdateRequest.aidl",
+        "src/android/net/IEthernetManager.aidl",
+        "src/android/net/IEthernetServiceListener.aidl",
+        "src/android/net/INetworkInterfaceOutcomeReceiver.aidl",
+        "src/android/net/ITetheredInterfaceCallback.aidl",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// Connectivity-T common libraries.
+
+filegroup {
+    name: "framework-connectivity-tiramisu-internal-sources",
+    srcs: [
+        "src/android/net/ConnectivityFrameworkInitializerTiramisu.java",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+filegroup {
+    name: "framework-connectivity-tiramisu-updatable-sources",
+    srcs: [
+        ":framework-connectivity-ethernet-sources",
+        ":framework-connectivity-ipsec-sources",
+        ":framework-connectivity-netstats-sources",
+        ":framework-connectivity-nsd-sources",
+        ":framework-connectivity-tiramisu-internal-sources",
+    ],
+    visibility: [
+        "//frameworks/base",
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+}
+
+cc_library_shared {
+    name: "libframework-connectivity-tiramisu-jni",
+    min_sdk_version: "30",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        // Don't warn about S API usage even with
+        // min_sdk 30: the library is only loaded
+        // on S+ devices
+        "-Wno-unguarded-availability",
+        "-Wthread-safety",
+    ],
+    srcs: [
+        "jni/android_net_TrafficStats.cpp",
+        "jni/onload.cpp",
+    ],
+    shared_libs: [
+        "libandroid",
+        "liblog",
+        "libnativehelper",
+    ],
+    stl: "none",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 1389ff2..eb77288 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -1,6 +1,74 @@
 // Signature format: 2.0
+package android.app.usage {
+
+  public final class NetworkStats implements java.lang.AutoCloseable {
+    method public void close();
+    method public boolean getNextBucket(@Nullable android.app.usage.NetworkStats.Bucket);
+    method public boolean hasNextBucket();
+  }
+
+  public static class NetworkStats.Bucket {
+    ctor public NetworkStats.Bucket();
+    method public int getDefaultNetworkStatus();
+    method public long getEndTimeStamp();
+    method public int getMetered();
+    method public int getRoaming();
+    method public long getRxBytes();
+    method public long getRxPackets();
+    method public long getStartTimeStamp();
+    method public int getState();
+    method public int getTag();
+    method public long getTxBytes();
+    method public long getTxPackets();
+    method public int getUid();
+    field public static final int DEFAULT_NETWORK_ALL = -1; // 0xffffffff
+    field public static final int DEFAULT_NETWORK_NO = 1; // 0x1
+    field public static final int DEFAULT_NETWORK_YES = 2; // 0x2
+    field public static final int METERED_ALL = -1; // 0xffffffff
+    field public static final int METERED_NO = 1; // 0x1
+    field public static final int METERED_YES = 2; // 0x2
+    field public static final int ROAMING_ALL = -1; // 0xffffffff
+    field public static final int ROAMING_NO = 1; // 0x1
+    field public static final int ROAMING_YES = 2; // 0x2
+    field public static final int STATE_ALL = -1; // 0xffffffff
+    field public static final int STATE_DEFAULT = 1; // 0x1
+    field public static final int STATE_FOREGROUND = 2; // 0x2
+    field public static final int TAG_NONE = 0; // 0x0
+    field public static final int UID_ALL = -1; // 0xffffffff
+    field public static final int UID_REMOVED = -4; // 0xfffffffc
+    field public static final int UID_TETHERING = -5; // 0xfffffffb
+  }
+
+  public class NetworkStatsManager {
+    method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, @Nullable String, long, long, int) throws java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, @Nullable String, long, long, int, int) throws java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, @Nullable String, long, long, int, int, int) throws java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats querySummary(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback);
+    method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
+    method public void unregisterUsageCallback(@NonNull android.app.usage.NetworkStatsManager.UsageCallback);
+  }
+
+  public abstract static class NetworkStatsManager.UsageCallback {
+    ctor public NetworkStatsManager.UsageCallback();
+    method public abstract void onThresholdReached(int, @Nullable String);
+  }
+
+}
+
 package android.net {
 
+  public final class EthernetNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+    ctor public EthernetNetworkSpecifier(@NonNull String);
+    method public int describeContents();
+    method @Nullable public String getInterfaceName();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.EthernetNetworkSpecifier> CREATOR;
+  }
+
   public final class IpSecAlgorithm implements android.os.Parcelable {
     ctor public IpSecAlgorithm(@NonNull String, @NonNull byte[]);
     ctor public IpSecAlgorithm(@NonNull String, @NonNull byte[], int);
@@ -70,16 +138,62 @@
     method @NonNull public android.net.IpSecTransform.Builder setIpv4Encapsulation(@NonNull android.net.IpSecManager.UdpEncapsulationSocket, int);
   }
 
+  public class TrafficStats {
+    ctor public TrafficStats();
+    method public static void clearThreadStatsTag();
+    method public static void clearThreadStatsUid();
+    method public static int getAndSetThreadStatsTag(int);
+    method public static long getMobileRxBytes();
+    method public static long getMobileRxPackets();
+    method public static long getMobileTxBytes();
+    method public static long getMobileTxPackets();
+    method public static long getRxBytes(@NonNull String);
+    method public static long getRxPackets(@NonNull String);
+    method public static int getThreadStatsTag();
+    method public static int getThreadStatsUid();
+    method public static long getTotalRxBytes();
+    method public static long getTotalRxPackets();
+    method public static long getTotalTxBytes();
+    method public static long getTotalTxPackets();
+    method public static long getTxBytes(@NonNull String);
+    method public static long getTxPackets(@NonNull String);
+    method public static long getUidRxBytes(int);
+    method public static long getUidRxPackets(int);
+    method @Deprecated public static long getUidTcpRxBytes(int);
+    method @Deprecated public static long getUidTcpRxSegments(int);
+    method @Deprecated public static long getUidTcpTxBytes(int);
+    method @Deprecated public static long getUidTcpTxSegments(int);
+    method public static long getUidTxBytes(int);
+    method public static long getUidTxPackets(int);
+    method @Deprecated public static long getUidUdpRxBytes(int);
+    method @Deprecated public static long getUidUdpRxPackets(int);
+    method @Deprecated public static long getUidUdpTxBytes(int);
+    method @Deprecated public static long getUidUdpTxPackets(int);
+    method public static void incrementOperationCount(int);
+    method public static void incrementOperationCount(int, int);
+    method public static void setThreadStatsTag(int);
+    method public static void setThreadStatsUid(int);
+    method public static void tagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
+    method public static void tagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
+    method public static void tagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
+    method public static void untagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
+    method public static void untagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
+    method public static void untagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
+    field public static final int UNSUPPORTED = -1; // 0xffffffff
+  }
+
 }
 
 package android.net.nsd {
 
   public final class NsdManager {
     method public void discoverServices(String, int, android.net.nsd.NsdManager.DiscoveryListener);
-    method public void discoverServices(@NonNull String, int, @Nullable android.net.Network, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
+    method public void discoverServices(@NonNull String, int, @Nullable android.net.Network, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
     method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
+    method public void registerService(@NonNull android.net.nsd.NsdServiceInfo, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.RegistrationListener);
     method public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
+    method public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener);
     method public void stopServiceDiscovery(android.net.nsd.NsdManager.DiscoveryListener);
     method public void unregisterService(android.net.nsd.NsdManager.RegistrationListener);
     field public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED";
diff --git a/framework-t/api/lint-baseline.txt b/framework-t/api/lint-baseline.txt
index 1e30747..2996a3e 100644
--- a/framework-t/api/lint-baseline.txt
+++ b/framework-t/api/lint-baseline.txt
@@ -1,7 +1,89 @@
 // Baseline format: 1.0
+BannedThrow: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+BannedThrow: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+BannedThrow: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+BannedThrow: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+BannedThrow: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+BannedThrow: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+BannedThrow: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+
+
+BuilderSetStyle: android.net.IpSecTransform.Builder#buildTransportModeTransform(java.net.InetAddress, android.net.IpSecManager.SecurityParameterIndex):
+    Builder methods names should use setFoo() / addFoo() / clearFoo() style: method android.net.IpSecTransform.Builder.buildTransportModeTransform(java.net.InetAddress,android.net.IpSecManager.SecurityParameterIndex)
+
+
+EqualsAndHashCode: android.net.IpSecTransform#equals(Object):
+    Must override both equals and hashCode; missing one in android.net.IpSecTransform
+
+
+ExecutorRegistration: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler):
+    Registration methods should have overload that accepts delivery Executor: `registerUsageCallback`
+
+
+GenericException: android.app.usage.NetworkStats#finalize():
+    Methods must not throw generic exceptions (`java.lang.Throwable`)
 GenericException: android.net.IpSecManager.SecurityParameterIndex#finalize():
     Methods must not throw generic exceptions (`java.lang.Throwable`)
 GenericException: android.net.IpSecManager.UdpEncapsulationSocket#finalize():
     Methods must not throw generic exceptions (`java.lang.Throwable`)
 GenericException: android.net.IpSecTransform#finalize():
     Methods must not throw generic exceptions (`java.lang.Throwable`)
+
+
+MissingBuildMethod: android.net.IpSecTransform.Builder:
+    android.net.IpSecTransform.Builder does not declare a `build()` method, but builder classes are expected to
+
+
+MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
+    Missing nullability on method `queryDetails` return
+MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
+    Missing nullability on method `querySummary` return
+MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
+    Missing nullability on method `querySummaryForDevice` return
+MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
+    Missing nullability on method `querySummaryForUser` return
+MissingNullability: android.net.IpSecAlgorithm#writeToParcel(android.os.Parcel, int) parameter #0:
+    Missing nullability on parameter `out` in method `writeToParcel`
+MissingNullability: android.net.IpSecManager.UdpEncapsulationSocket#getFileDescriptor():
+    Missing nullability on method `getFileDescriptor` return
+
+
+RethrowRemoteException: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
+    Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)
+RethrowRemoteException: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
+    Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)
+RethrowRemoteException: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
+    Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)
+RethrowRemoteException: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
+    Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)
+
+
+StaticFinalBuilder: android.net.IpSecTransform.Builder:
+    Builder must be final: android.net.IpSecTransform.Builder
+
+
+StaticUtils: android.net.TrafficStats:
+    Fully-static utility classes must not have constructor
+
+
+UseParcelFileDescriptor: android.net.IpSecManager#applyTransportModeTransform(java.io.FileDescriptor, int, android.net.IpSecTransform) parameter #0:
+    Must use ParcelFileDescriptor instead of FileDescriptor in parameter socket in android.net.IpSecManager.applyTransportModeTransform(java.io.FileDescriptor socket, int direction, android.net.IpSecTransform transform)
+UseParcelFileDescriptor: android.net.IpSecManager#removeTransportModeTransforms(java.io.FileDescriptor) parameter #0:
+    Must use ParcelFileDescriptor instead of FileDescriptor in parameter socket in android.net.IpSecManager.removeTransportModeTransforms(java.io.FileDescriptor socket)
+UseParcelFileDescriptor: android.net.IpSecManager.UdpEncapsulationSocket#getFileDescriptor():
+    Must use ParcelFileDescriptor instead of FileDescriptor in method android.net.IpSecManager.UdpEncapsulationSocket.getFileDescriptor()
+UseParcelFileDescriptor: android.net.TrafficStats#tagFileDescriptor(java.io.FileDescriptor) parameter #0:
+    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in android.net.TrafficStats.tagFileDescriptor(java.io.FileDescriptor fd)
+UseParcelFileDescriptor: android.net.TrafficStats#untagFileDescriptor(java.io.FileDescriptor) parameter #0:
+    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in android.net.TrafficStats.untagFileDescriptor(java.io.FileDescriptor fd)
+UseParcelFileDescriptor: com.android.server.NetworkManagementSocketTagger#tag(java.io.FileDescriptor) parameter #0:
+    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in com.android.server.NetworkManagementSocketTagger.tag(java.io.FileDescriptor fd)
+UseParcelFileDescriptor: com.android.server.NetworkManagementSocketTagger#untag(java.io.FileDescriptor) parameter #0:
+    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in com.android.server.NetworkManagementSocketTagger.untag(java.io.FileDescriptor fd)
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 2ff3451..c1f7b39 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -1,10 +1,60 @@
 // Signature format: 2.0
+package android.app.usage {
+
+  public class NetworkStatsManager {
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void forceUpdate();
+    method public static int getCollapsedRatType(int);
+    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getMobileUidStats();
+    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getWifiUidStats();
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void noteUidForeground(int, boolean);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void notifyNetworkStatus(@NonNull java.util.List<android.net.Network>, @NonNull java.util.List<android.net.NetworkStateSnapshot>, @Nullable String, @NonNull java.util.List<android.net.UnderlyingNetworkInfo>);
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForDevice(@NonNull android.net.NetworkTemplate, long, long);
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(@NonNull android.net.NetworkTemplate, long, long, int, int, int) throws java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats querySummary(@NonNull android.net.NetworkTemplate, long, long) throws java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(@NonNull android.net.NetworkTemplate, long, long);
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryTaggedSummary(@NonNull android.net.NetworkTemplate, long, long) throws java.lang.SecurityException;
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}, conditional=true) public void registerUsageCallback(@NonNull android.net.NetworkTemplate, long, @NonNull java.util.concurrent.Executor, @NonNull android.app.usage.NetworkStatsManager.UsageCallback);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void setDefaultGlobalAlert(long);
+    method public void setPollForce(boolean);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void setPollOnOpen(boolean);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void setStatsProviderWarningAndLimitAsync(@NonNull String, long, long);
+    field public static final int NETWORK_TYPE_5G_NSA = -2; // 0xfffffffe
+  }
+
+  public abstract static class NetworkStatsManager.UsageCallback {
+    method public void onThresholdReached(@NonNull android.net.NetworkTemplate);
+  }
+
+}
+
 package android.net {
 
   public final class ConnectivityFrameworkInitializerTiramisu {
     method public static void registerServiceWrappers();
   }
 
+  public class EthernetManager {
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void addEthernetStateListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void addInterfaceStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.net.EthernetManager.InterfaceStateListener);
+    method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public java.util.List<java.lang.String> getInterfaceList();
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void removeEthernetStateListener(@NonNull java.util.function.IntConsumer);
+    method public void removeInterfaceStateListener(@NonNull android.net.EthernetManager.InterfaceStateListener);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setEthernetEnabled(boolean);
+    method public void setIncludeTestInterfaces(boolean);
+    field public static final int ETHERNET_STATE_DISABLED = 0; // 0x0
+    field public static final int ETHERNET_STATE_ENABLED = 1; // 0x1
+    field public static final int ROLE_CLIENT = 1; // 0x1
+    field public static final int ROLE_NONE = 0; // 0x0
+    field public static final int ROLE_SERVER = 2; // 0x2
+    field public static final int STATE_ABSENT = 0; // 0x0
+    field public static final int STATE_LINK_DOWN = 1; // 0x1
+    field public static final int STATE_LINK_UP = 2; // 0x2
+  }
+
+  public static interface EthernetManager.InterfaceStateListener {
+    method public void onInterfaceStateChanged(@NonNull String, int, int, @Nullable android.net.IpConfiguration);
+  }
+
   public class IpSecManager {
     field public static final int DIRECTION_FWD = 2; // 0x2
   }
@@ -13,5 +63,139 @@
     method public int getResourceId();
   }
 
+  public class NetworkIdentity {
+    method public int getOemManaged();
+    method public int getRatType();
+    method public int getSubId();
+    method @Nullable public String getSubscriberId();
+    method public int getType();
+    method @Nullable public String getWifiNetworkKey();
+    method public boolean isDefaultNetwork();
+    method public boolean isMetered();
+    method public boolean isRoaming();
+  }
+
+  public static final class NetworkIdentity.Builder {
+    ctor public NetworkIdentity.Builder();
+    method @NonNull public android.net.NetworkIdentity build();
+    method @NonNull public android.net.NetworkIdentity.Builder clearRatType();
+    method @NonNull public android.net.NetworkIdentity.Builder setDefaultNetwork(boolean);
+    method @NonNull public android.net.NetworkIdentity.Builder setMetered(boolean);
+    method @NonNull public android.net.NetworkIdentity.Builder setNetworkStateSnapshot(@NonNull android.net.NetworkStateSnapshot);
+    method @NonNull public android.net.NetworkIdentity.Builder setOemManaged(int);
+    method @NonNull public android.net.NetworkIdentity.Builder setRatType(int);
+    method @NonNull public android.net.NetworkIdentity.Builder setRoaming(boolean);
+    method @NonNull public android.net.NetworkIdentity.Builder setSubId(int);
+    method @NonNull public android.net.NetworkIdentity.Builder setSubscriberId(@Nullable String);
+    method @NonNull public android.net.NetworkIdentity.Builder setType(int);
+    method @NonNull public android.net.NetworkIdentity.Builder setWifiNetworkKey(@Nullable String);
+  }
+
+  public final class NetworkStateSnapshot implements android.os.Parcelable {
+    ctor public NetworkStateSnapshot(@NonNull android.net.Network, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @Nullable String, int);
+    method public int describeContents();
+    method public int getLegacyType();
+    method @NonNull public android.net.LinkProperties getLinkProperties();
+    method @NonNull public android.net.Network getNetwork();
+    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
+    method public int getSubId();
+    method @Deprecated @Nullable public String getSubscriberId();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkStateSnapshot> CREATOR;
+  }
+
+  public class NetworkStatsCollection {
+    method @NonNull public java.util.Map<android.net.NetworkStatsCollection.Key,android.net.NetworkStatsHistory> getEntries();
+  }
+
+  public static final class NetworkStatsCollection.Builder {
+    ctor public NetworkStatsCollection.Builder(long);
+    method @NonNull public android.net.NetworkStatsCollection.Builder addEntry(@NonNull android.net.NetworkStatsCollection.Key, @NonNull android.net.NetworkStatsHistory);
+    method @NonNull public android.net.NetworkStatsCollection build();
+  }
+
+  public static final class NetworkStatsCollection.Key {
+    ctor public NetworkStatsCollection.Key(@NonNull java.util.Set<android.net.NetworkIdentity>, int, int, int);
+  }
+
+  public final class NetworkStatsHistory implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public java.util.List<android.net.NetworkStatsHistory.Entry> getEntries();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkStatsHistory> CREATOR;
+  }
+
+  public static final class NetworkStatsHistory.Builder {
+    ctor public NetworkStatsHistory.Builder(long, int);
+    method @NonNull public android.net.NetworkStatsHistory.Builder addEntry(@NonNull android.net.NetworkStatsHistory.Entry);
+    method @NonNull public android.net.NetworkStatsHistory build();
+  }
+
+  public static final class NetworkStatsHistory.Entry {
+    ctor public NetworkStatsHistory.Entry(long, long, long, long, long, long, long);
+    method public long getActiveTime();
+    method public long getBucketStart();
+    method public long getOperations();
+    method public long getRxBytes();
+    method public long getRxPackets();
+    method public long getTxBytes();
+    method public long getTxPackets();
+  }
+
+  public final class NetworkTemplate implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getDefaultNetworkStatus();
+    method public int getMatchRule();
+    method public int getMeteredness();
+    method public int getOemManaged();
+    method public int getRatType();
+    method public int getRoaming();
+    method @NonNull public java.util.Set<java.lang.String> getSubscriberIds();
+    method @NonNull public java.util.Set<java.lang.String> getWifiNetworkKeys();
+    method public boolean matches(@NonNull android.net.NetworkIdentity);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkTemplate> CREATOR;
+    field public static final int MATCH_BLUETOOTH = 8; // 0x8
+    field public static final int MATCH_CARRIER = 10; // 0xa
+    field public static final int MATCH_ETHERNET = 5; // 0x5
+    field public static final int MATCH_MOBILE = 1; // 0x1
+    field public static final int MATCH_PROXY = 9; // 0x9
+    field public static final int MATCH_WIFI = 4; // 0x4
+    field public static final int NETWORK_TYPE_ALL = -1; // 0xffffffff
+    field public static final int OEM_MANAGED_ALL = -1; // 0xffffffff
+    field public static final int OEM_MANAGED_NO = 0; // 0x0
+    field public static final int OEM_MANAGED_PAID = 1; // 0x1
+    field public static final int OEM_MANAGED_PRIVATE = 2; // 0x2
+    field public static final int OEM_MANAGED_YES = -2; // 0xfffffffe
+  }
+
+  public static final class NetworkTemplate.Builder {
+    ctor public NetworkTemplate.Builder(int);
+    method @NonNull public android.net.NetworkTemplate build();
+    method @NonNull public android.net.NetworkTemplate.Builder setDefaultNetworkStatus(int);
+    method @NonNull public android.net.NetworkTemplate.Builder setMeteredness(int);
+    method @NonNull public android.net.NetworkTemplate.Builder setOemManaged(int);
+    method @NonNull public android.net.NetworkTemplate.Builder setRatType(int);
+    method @NonNull public android.net.NetworkTemplate.Builder setRoaming(int);
+    method @NonNull public android.net.NetworkTemplate.Builder setSubscriberIds(@NonNull java.util.Set<java.lang.String>);
+    method @NonNull public android.net.NetworkTemplate.Builder setWifiNetworkKeys(@NonNull java.util.Set<java.lang.String>);
+  }
+
+  public class TrafficStats {
+    method public static void attachSocketTagger();
+    method public static void init(@NonNull android.content.Context);
+    method public static void setThreadStatsTagDownload();
+  }
+
+  public final class UnderlyingNetworkInfo implements android.os.Parcelable {
+    ctor public UnderlyingNetworkInfo(int, @NonNull String, @NonNull java.util.List<java.lang.String>);
+    method public int describeContents();
+    method @NonNull public String getInterface();
+    method public int getOwnerUid();
+    method @NonNull public java.util.List<java.lang.String> getUnderlyingInterfaces();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.UnderlyingNetworkInfo> CREATOR;
+  }
+
 }
 
diff --git a/framework-t/api/module-lib-lint-baseline.txt b/framework-t/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..3158bd4
--- /dev/null
+++ b/framework-t/api/module-lib-lint-baseline.txt
@@ -0,0 +1,7 @@
+// Baseline format: 1.0
+BannedThrow: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(android.net.NetworkTemplate, long, long, int, int, int):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+BannedThrow: android.app.usage.NetworkStatsManager#querySummary(android.net.NetworkTemplate, long, long):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+BannedThrow: android.app.usage.NetworkStatsManager#queryTaggedSummary(android.net.NetworkTemplate, long, long):
+    Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
diff --git a/framework-t/api/removed.txt b/framework-t/api/removed.txt
index d802177..1ba87d8 100644
--- a/framework-t/api/removed.txt
+++ b/framework-t/api/removed.txt
@@ -1 +1,9 @@
 // Signature format: 2.0
+package android.net {
+
+  public class TrafficStats {
+    method @Deprecated public static void setThreadStatsUidSelf();
+  }
+
+}
+
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 041bcaf..6460fed 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -1,6 +1,54 @@
 // Signature format: 2.0
+package android.app.usage {
+
+  public class NetworkStatsManager {
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void registerNetworkStatsProvider(@NonNull String, @NonNull android.net.netstats.provider.NetworkStatsProvider);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
+  }
+
+}
+
 package android.net {
 
+  public class EthernetManager {
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) public void disableInterface(@NonNull String, @Nullable java.util.concurrent.Executor, @Nullable android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) public void enableInterface(@NonNull String, @Nullable java.util.concurrent.Executor, @Nullable android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>);
+    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public android.net.EthernetManager.TetheredInterfaceRequest requestTetheredInterface(@NonNull java.util.concurrent.Executor, @NonNull android.net.EthernetManager.TetheredInterfaceCallback);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) public void updateConfiguration(@NonNull String, @NonNull android.net.EthernetNetworkUpdateRequest, @Nullable java.util.concurrent.Executor, @Nullable android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>);
+  }
+
+  public static interface EthernetManager.TetheredInterfaceCallback {
+    method public void onAvailable(@NonNull String);
+    method public void onUnavailable();
+  }
+
+  public static class EthernetManager.TetheredInterfaceRequest {
+    method public void release();
+  }
+
+  public final class EthernetNetworkManagementException extends java.lang.RuntimeException implements android.os.Parcelable {
+    ctor public EthernetNetworkManagementException(@NonNull String);
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.EthernetNetworkManagementException> CREATOR;
+  }
+
+  public final class EthernetNetworkUpdateRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.net.IpConfiguration getIpConfiguration();
+    method @Nullable public android.net.NetworkCapabilities getNetworkCapabilities();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.EthernetNetworkUpdateRequest> CREATOR;
+  }
+
+  public static final class EthernetNetworkUpdateRequest.Builder {
+    ctor public EthernetNetworkUpdateRequest.Builder();
+    ctor public EthernetNetworkUpdateRequest.Builder(@NonNull android.net.EthernetNetworkUpdateRequest);
+    method @NonNull public android.net.EthernetNetworkUpdateRequest build();
+    method @NonNull public android.net.EthernetNetworkUpdateRequest.Builder setIpConfiguration(@Nullable android.net.IpConfiguration);
+    method @NonNull public android.net.EthernetNetworkUpdateRequest.Builder setNetworkCapabilities(@Nullable android.net.NetworkCapabilities);
+  }
+
   public class IpSecManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void applyTunnelModeTransform(@NonNull android.net.IpSecManager.IpSecTunnelInterface, int, @NonNull android.net.IpSecTransform) throws java.io.IOException;
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public android.net.IpSecManager.IpSecTunnelInterface createIpSecTunnelInterface(@NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull android.net.Network) throws java.io.IOException, android.net.IpSecManager.ResourceUnavailableException;
@@ -18,5 +66,76 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public android.net.IpSecTransform buildTunnelModeTransform(@NonNull java.net.InetAddress, @NonNull android.net.IpSecManager.SecurityParameterIndex) throws java.io.IOException, android.net.IpSecManager.ResourceUnavailableException, android.net.IpSecManager.SpiUnavailableException;
   }
 
+  public final class NetworkStats implements java.lang.Iterable<android.net.NetworkStats.Entry> android.os.Parcelable {
+    ctor public NetworkStats(long, int);
+    method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
+    method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
+    method public int describeContents();
+    method @NonNull public java.util.Iterator<android.net.NetworkStats.Entry> iterator();
+    method @NonNull public android.net.NetworkStats subtract(@NonNull android.net.NetworkStats);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkStats> CREATOR;
+    field public static final int DEFAULT_NETWORK_ALL = -1; // 0xffffffff
+    field public static final int DEFAULT_NETWORK_NO = 0; // 0x0
+    field public static final int DEFAULT_NETWORK_YES = 1; // 0x1
+    field public static final String IFACE_VT = "vt_data0";
+    field public static final int METERED_ALL = -1; // 0xffffffff
+    field public static final int METERED_NO = 0; // 0x0
+    field public static final int METERED_YES = 1; // 0x1
+    field public static final int ROAMING_ALL = -1; // 0xffffffff
+    field public static final int ROAMING_NO = 0; // 0x0
+    field public static final int ROAMING_YES = 1; // 0x1
+    field public static final int SET_ALL = -1; // 0xffffffff
+    field public static final int SET_DEFAULT = 0; // 0x0
+    field public static final int SET_FOREGROUND = 1; // 0x1
+    field public static final int TAG_NONE = 0; // 0x0
+    field public static final int UID_ALL = -1; // 0xffffffff
+    field public static final int UID_TETHERING = -5; // 0xfffffffb
+  }
+
+  public static class NetworkStats.Entry {
+    ctor public NetworkStats.Entry(@Nullable String, int, int, int, int, int, int, long, long, long, long, long);
+    method public int getDefaultNetwork();
+    method public int getMetered();
+    method public long getOperations();
+    method public int getRoaming();
+    method public long getRxBytes();
+    method public long getRxPackets();
+    method public int getSet();
+    method public int getTag();
+    method public long getTxBytes();
+    method public long getTxPackets();
+    method public int getUid();
+  }
+
+  public class TrafficStats {
+    method public static void setThreadStatsTagApp();
+    method public static void setThreadStatsTagBackup();
+    method public static void setThreadStatsTagRestore();
+    field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_END = -113; // 0xffffff8f
+    field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_START = -128; // 0xffffff80
+    field public static final int TAG_NETWORK_STACK_RANGE_END = -257; // 0xfffffeff
+    field public static final int TAG_NETWORK_STACK_RANGE_START = -768; // 0xfffffd00
+    field public static final int TAG_SYSTEM_IMPERSONATION_RANGE_END = -241; // 0xffffff0f
+    field public static final int TAG_SYSTEM_IMPERSONATION_RANGE_START = -256; // 0xffffff00
+  }
+
+}
+
+package android.net.netstats.provider {
+
+  public abstract class NetworkStatsProvider {
+    ctor public NetworkStatsProvider();
+    method public void notifyAlertReached();
+    method public void notifyLimitReached();
+    method public void notifyStatsUpdated(int, @NonNull android.net.NetworkStats, @NonNull android.net.NetworkStats);
+    method public void notifyWarningReached();
+    method public abstract void onRequestStatsUpdate(int);
+    method public abstract void onSetAlert(long);
+    method public abstract void onSetLimit(@NonNull String, long);
+    method public void onSetWarningAndLimit(@NonNull String, long, long);
+    field public static final int QUOTA_UNLIMITED = -1; // 0xffffffff
+  }
+
 }
 
diff --git a/framework-t/jni/android_net_TrafficStats.cpp b/framework-t/jni/android_net_TrafficStats.cpp
new file mode 100644
index 0000000..f3c58b1
--- /dev/null
+++ b/framework-t/jni/android_net_TrafficStats.cpp
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <android/file_descriptor_jni.h>
+#include <android/multinetwork.h>
+#include <nativehelper/JNIHelp.h>
+
+namespace android {
+
+static jint tagSocketFd(JNIEnv* env, jclass, jobject fileDescriptor, jint tag, jint uid) {
+  int fd = AFileDescriptor_getFd(env, fileDescriptor);
+  if (fd == -1) return -EBADF;
+  return android_tag_socket_with_uid(fd, tag, uid);
+}
+
+static jint untagSocketFd(JNIEnv* env, jclass, jobject fileDescriptor) {
+  int fd = AFileDescriptor_getFd(env, fileDescriptor);
+  if (fd == -1) return -EBADF;
+  return android_untag_socket(fd);
+}
+
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    { "native_tagSocketFd", "(Ljava/io/FileDescriptor;II)I", (void*) tagSocketFd },
+    { "native_untagSocketFd", "(Ljava/io/FileDescriptor;)I", (void*) untagSocketFd },
+};
+
+int register_android_net_TrafficStats(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "android/net/TrafficStats", gMethods, NELEM(gMethods));
+}
+
+};  // namespace android
+
diff --git a/framework-t/jni/onload.cpp b/framework-t/jni/onload.cpp
new file mode 100644
index 0000000..1fb42c6
--- /dev/null
+++ b/framework-t/jni/onload.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "FrameworkConnectivityJNI"
+
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+
+namespace android {
+
+int register_android_net_TrafficStats(JNIEnv* env);
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+    JNIEnv *env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "ERROR: GetEnv failed");
+        return JNI_ERR;
+    }
+
+    if (register_android_net_TrafficStats(env) < 0) return JNI_ERR;
+
+    return JNI_VERSION_1_6;
+}
+
+};  // namespace android
+
diff --git a/framework-t/src/android/app/usage/NetworkStats.java b/framework-t/src/android/app/usage/NetworkStats.java
new file mode 100644
index 0000000..74fe4bd
--- /dev/null
+++ b/framework-t/src/android/app/usage/NetworkStats.java
@@ -0,0 +1,744 @@
+/**
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.INetworkStatsService;
+import android.net.INetworkStatsSession;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TrafficStats;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.net.module.util.CollectionUtils;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+
+/**
+ * Class providing enumeration over buckets of network usage statistics. {@link NetworkStats} objects
+ * are returned as results to various queries in {@link NetworkStatsManager}.
+ */
+public final class NetworkStats implements AutoCloseable {
+    private final static String TAG = "NetworkStats";
+
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+
+    /**
+     * Start timestamp of stats collected
+     */
+    private final long mStartTimeStamp;
+
+    /**
+     * End timestamp of stats collected
+     */
+    private final long mEndTimeStamp;
+
+    /**
+     * Non-null array indicates the query enumerates over uids.
+     */
+    private int[] mUids;
+
+    /**
+     * Index of the current uid in mUids when doing uid enumeration or a single uid value,
+     * depending on query type.
+     */
+    private int mUidOrUidIndex;
+
+    /**
+     * Tag id in case if was specified in the query.
+     */
+    private int mTag = android.net.NetworkStats.TAG_NONE;
+
+    /**
+     * State in case it was not specified in the query.
+     */
+    private int mState = Bucket.STATE_ALL;
+
+    /**
+     * The session while the query requires it, null if all the stats have been collected or close()
+     * has been called.
+     */
+    private INetworkStatsSession mSession;
+    private NetworkTemplate mTemplate;
+
+    /**
+     * Results of a summary query.
+     */
+    private android.net.NetworkStats mSummary = null;
+
+    /**
+     * Results of detail queries.
+     */
+    private NetworkStatsHistory mHistory = null;
+
+    /**
+     * Where we are in enumerating over the current result.
+     */
+    private int mEnumerationIndex = 0;
+
+    /**
+     * Recycling entry objects to prevent heap fragmentation.
+     */
+    private android.net.NetworkStats.Entry mRecycledSummaryEntry = null;
+    private NetworkStatsHistory.Entry mRecycledHistoryEntry = null;
+
+    /** @hide */
+    NetworkStats(Context context, NetworkTemplate template, int flags, long startTimestamp,
+            long endTimestamp, INetworkStatsService statsService)
+            throws RemoteException, SecurityException {
+        // Open network stats session
+        mSession = statsService.openSessionForUsageStats(flags, context.getOpPackageName());
+        mCloseGuard.open("close");
+        mTemplate = template;
+        mStartTimeStamp = startTimestamp;
+        mEndTimeStamp = endTimestamp;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    // -------------------------BEGINNING OF PUBLIC API-----------------------------------
+
+    /**
+     * Buckets are the smallest elements of a query result. As some dimensions of a result may be
+     * aggregated (e.g. time or state) some values may be equal across all buckets.
+     */
+    public static class Bucket {
+        /** @hide */
+        @IntDef(prefix = { "STATE_" }, value = {
+                STATE_ALL,
+                STATE_DEFAULT,
+                STATE_FOREGROUND
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface State {}
+
+        /**
+         * Combined usage across all states.
+         */
+        public static final int STATE_ALL = -1;
+
+        /**
+         * Usage not accounted for in any other state.
+         */
+        public static final int STATE_DEFAULT = 0x1;
+
+        /**
+         * Foreground usage.
+         */
+        public static final int STATE_FOREGROUND = 0x2;
+
+        /**
+         * Special UID value for aggregate/unspecified.
+         */
+        public static final int UID_ALL = android.net.NetworkStats.UID_ALL;
+
+        /**
+         * Special UID value for removed apps.
+         */
+        public static final int UID_REMOVED = TrafficStats.UID_REMOVED;
+
+        /**
+         * Special UID value for data usage by tethering.
+         */
+        public static final int UID_TETHERING = TrafficStats.UID_TETHERING;
+
+        /** @hide */
+        @IntDef(prefix = { "METERED_" }, value = {
+                METERED_ALL,
+                METERED_NO,
+                METERED_YES
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface Metered {}
+
+        /**
+         * Combined usage across all metered states. Covers metered and unmetered usage.
+         */
+        public static final int METERED_ALL = -1;
+
+        /**
+         * Usage that occurs on an unmetered network.
+         */
+        public static final int METERED_NO = 0x1;
+
+        /**
+         * Usage that occurs on a metered network.
+         *
+         * <p>A network is classified as metered when the user is sensitive to heavy data usage on
+         * that connection.
+         */
+        public static final int METERED_YES = 0x2;
+
+        /** @hide */
+        @IntDef(prefix = { "ROAMING_" }, value = {
+                ROAMING_ALL,
+                ROAMING_NO,
+                ROAMING_YES
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface Roaming {}
+
+        /**
+         * Combined usage across all roaming states. Covers both roaming and non-roaming usage.
+         */
+        public static final int ROAMING_ALL = -1;
+
+        /**
+         * Usage that occurs on a home, non-roaming network.
+         *
+         * <p>Any cellular usage in this bucket was incurred while the device was connected to a
+         * tower owned or operated by the user's wireless carrier, or a tower that the user's
+         * wireless carrier has indicated should be treated as a home network regardless.
+         *
+         * <p>This is also the default value for network types that do not support roaming.
+         */
+        public static final int ROAMING_NO = 0x1;
+
+        /**
+         * Usage that occurs on a roaming network.
+         *
+         * <p>Any cellular usage in this bucket as incurred while the device was roaming on another
+         * carrier's network, for which additional charges may apply.
+         */
+        public static final int ROAMING_YES = 0x2;
+
+        /** @hide */
+        @IntDef(prefix = { "DEFAULT_NETWORK_" }, value = {
+                DEFAULT_NETWORK_ALL,
+                DEFAULT_NETWORK_NO,
+                DEFAULT_NETWORK_YES
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface DefaultNetworkStatus {}
+
+        /**
+         * Combined usage for this network regardless of default network status.
+         */
+        public static final int DEFAULT_NETWORK_ALL = -1;
+
+        /**
+         * Usage that occurs while this network is not a default network.
+         *
+         * <p>This implies that the app responsible for this usage requested that it occur on a
+         * specific network different from the one(s) the system would have selected for it.
+         */
+        public static final int DEFAULT_NETWORK_NO = 0x1;
+
+        /**
+         * Usage that occurs while this network is a default network.
+         *
+         * <p>This implies that the app either did not select a specific network for this usage,
+         * or it selected a network that the system could have selected for app traffic.
+         */
+        public static final int DEFAULT_NETWORK_YES = 0x2;
+
+        /**
+         * Special TAG value for total data across all tags
+         */
+        public static final int TAG_NONE = android.net.NetworkStats.TAG_NONE;
+
+        private int mUid;
+        private int mTag;
+        private int mState;
+        private int mDefaultNetworkStatus;
+        private int mMetered;
+        private int mRoaming;
+        private long mBeginTimeStamp;
+        private long mEndTimeStamp;
+        private long mRxBytes;
+        private long mRxPackets;
+        private long mTxBytes;
+        private long mTxPackets;
+
+        private static int convertSet(@State int state) {
+            switch (state) {
+                case STATE_ALL: return android.net.NetworkStats.SET_ALL;
+                case STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
+                case STATE_FOREGROUND: return android.net.NetworkStats.SET_FOREGROUND;
+            }
+            return 0;
+        }
+
+        private static @State int convertState(int networkStatsSet) {
+            switch (networkStatsSet) {
+                case android.net.NetworkStats.SET_ALL : return STATE_ALL;
+                case android.net.NetworkStats.SET_DEFAULT : return STATE_DEFAULT;
+                case android.net.NetworkStats.SET_FOREGROUND : return STATE_FOREGROUND;
+            }
+            return 0;
+        }
+
+        private static int convertUid(int uid) {
+            switch (uid) {
+                case TrafficStats.UID_REMOVED: return UID_REMOVED;
+                case TrafficStats.UID_TETHERING: return UID_TETHERING;
+            }
+            return uid;
+        }
+
+        private static int convertTag(int tag) {
+            switch (tag) {
+                case android.net.NetworkStats.TAG_NONE: return TAG_NONE;
+            }
+            return tag;
+        }
+
+        private static @Metered int convertMetered(int metered) {
+            switch (metered) {
+                case android.net.NetworkStats.METERED_ALL : return METERED_ALL;
+                case android.net.NetworkStats.METERED_NO: return METERED_NO;
+                case android.net.NetworkStats.METERED_YES: return METERED_YES;
+            }
+            return 0;
+        }
+
+        private static @Roaming int convertRoaming(int roaming) {
+            switch (roaming) {
+                case android.net.NetworkStats.ROAMING_ALL : return ROAMING_ALL;
+                case android.net.NetworkStats.ROAMING_NO: return ROAMING_NO;
+                case android.net.NetworkStats.ROAMING_YES: return ROAMING_YES;
+            }
+            return 0;
+        }
+
+        private static @DefaultNetworkStatus int convertDefaultNetworkStatus(
+                int defaultNetworkStatus) {
+            switch (defaultNetworkStatus) {
+                case android.net.NetworkStats.DEFAULT_NETWORK_ALL : return DEFAULT_NETWORK_ALL;
+                case android.net.NetworkStats.DEFAULT_NETWORK_NO: return DEFAULT_NETWORK_NO;
+                case android.net.NetworkStats.DEFAULT_NETWORK_YES: return DEFAULT_NETWORK_YES;
+            }
+            return 0;
+        }
+
+        public Bucket() {
+        }
+
+        /**
+         * Key of the bucket. Usually an app uid or one of the following special values:<p />
+         * <ul>
+         * <li>{@link #UID_REMOVED}</li>
+         * <li>{@link #UID_TETHERING}</li>
+         * <li>{@link android.os.Process#SYSTEM_UID}</li>
+         * </ul>
+         * @return Bucket key.
+         */
+        public int getUid() {
+            return mUid;
+        }
+
+        /**
+         * Tag of the bucket.<p />
+         * @return Bucket tag.
+         */
+        public int getTag() {
+            return mTag;
+        }
+
+        /**
+         * Usage state. One of the following values:<p/>
+         * <ul>
+         * <li>{@link #STATE_ALL}</li>
+         * <li>{@link #STATE_DEFAULT}</li>
+         * <li>{@link #STATE_FOREGROUND}</li>
+         * </ul>
+         * @return Usage state.
+         */
+        public @State int getState() {
+            return mState;
+        }
+
+        /**
+         * Metered state. One of the following values:<p/>
+         * <ul>
+         * <li>{@link #METERED_ALL}</li>
+         * <li>{@link #METERED_NO}</li>
+         * <li>{@link #METERED_YES}</li>
+         * </ul>
+         * <p>A network is classified as metered when the user is sensitive to heavy data usage on
+         * that connection. Apps may warn before using these networks for large downloads. The
+         * metered state can be set by the user within data usage network restrictions.
+         */
+        public @Metered int getMetered() {
+            return mMetered;
+        }
+
+        /**
+         * Roaming state. One of the following values:<p/>
+         * <ul>
+         * <li>{@link #ROAMING_ALL}</li>
+         * <li>{@link #ROAMING_NO}</li>
+         * <li>{@link #ROAMING_YES}</li>
+         * </ul>
+         */
+        public @Roaming int getRoaming() {
+            return mRoaming;
+        }
+
+        /**
+         * Default network status. One of the following values:<p/>
+         * <ul>
+         * <li>{@link #DEFAULT_NETWORK_ALL}</li>
+         * <li>{@link #DEFAULT_NETWORK_NO}</li>
+         * <li>{@link #DEFAULT_NETWORK_YES}</li>
+         * </ul>
+         */
+        public @DefaultNetworkStatus int getDefaultNetworkStatus() {
+            return mDefaultNetworkStatus;
+        }
+
+        /**
+         * Start timestamp of the bucket's time interval. Defined in terms of "Unix time", see
+         * {@link java.lang.System#currentTimeMillis}.
+         * @return Start of interval.
+         */
+        public long getStartTimeStamp() {
+            return mBeginTimeStamp;
+        }
+
+        /**
+         * End timestamp of the bucket's time interval. Defined in terms of "Unix time", see
+         * {@link java.lang.System#currentTimeMillis}.
+         * @return End of interval.
+         */
+        public long getEndTimeStamp() {
+            return mEndTimeStamp;
+        }
+
+        /**
+         * Number of bytes received during the bucket's time interval. Statistics are measured at
+         * the network layer, so they include both TCP and UDP usage.
+         * @return Number of bytes.
+         */
+        public long getRxBytes() {
+            return mRxBytes;
+        }
+
+        /**
+         * Number of bytes transmitted during the bucket's time interval. Statistics are measured at
+         * the network layer, so they include both TCP and UDP usage.
+         * @return Number of bytes.
+         */
+        public long getTxBytes() {
+            return mTxBytes;
+        }
+
+        /**
+         * Number of packets received during the bucket's time interval. Statistics are measured at
+         * the network layer, so they include both TCP and UDP usage.
+         * @return Number of packets.
+         */
+        public long getRxPackets() {
+            return mRxPackets;
+        }
+
+        /**
+         * Number of packets transmitted during the bucket's time interval. Statistics are measured
+         * at the network layer, so they include both TCP and UDP usage.
+         * @return Number of packets.
+         */
+        public long getTxPackets() {
+            return mTxPackets;
+        }
+    }
+
+    /**
+     * Fills the recycled bucket with data of the next bin in the enumeration.
+     * @param bucketOut Bucket to be filled with data. If null, the method does
+     *                  nothing and returning false.
+     * @return true if successfully filled the bucket, false otherwise.
+     */
+    public boolean getNextBucket(@Nullable Bucket bucketOut) {
+        if (mSummary != null) {
+            return getNextSummaryBucket(bucketOut);
+        } else {
+            return getNextHistoryBucket(bucketOut);
+        }
+    }
+
+    /**
+     * Check if it is possible to ask for a next bucket in the enumeration.
+     * @return true if there is at least one more bucket.
+     */
+    public boolean hasNextBucket() {
+        if (mSummary != null) {
+            return mEnumerationIndex < mSummary.size();
+        } else if (mHistory != null) {
+            return mEnumerationIndex < mHistory.size()
+                    || hasNextUid();
+        }
+        return false;
+    }
+
+    /**
+     * Closes the enumeration. Call this method before this object gets out of scope.
+     */
+    @Override
+    public void close() {
+        if (mSession != null) {
+            try {
+                mSession.close();
+            } catch (RemoteException e) {
+                Log.w(TAG, e);
+                // Otherwise, meh
+            }
+        }
+        mSession = null;
+        if (mCloseGuard != null) {
+            mCloseGuard.close();
+        }
+    }
+
+    // -------------------------END OF PUBLIC API-----------------------------------
+
+    /**
+     * Collects device summary results into a Bucket.
+     * @throws RemoteException
+     */
+    Bucket getDeviceSummaryForNetwork() throws RemoteException {
+        mSummary = mSession.getDeviceSummaryForNetwork(mTemplate, mStartTimeStamp, mEndTimeStamp);
+
+        // Setting enumeration index beyond end to avoid accidental enumeration over data that does
+        // not belong to the calling user.
+        mEnumerationIndex = mSummary.size();
+
+        return getSummaryAggregate();
+    }
+
+    /**
+     * Collects summary results and sets summary enumeration mode.
+     * @throws RemoteException
+     */
+    void startSummaryEnumeration() throws RemoteException {
+        mSummary = mSession.getSummaryForAllUid(mTemplate, mStartTimeStamp, mEndTimeStamp,
+                false /* includeTags */);
+        mEnumerationIndex = 0;
+    }
+
+    /**
+     * Collects tagged summary results and sets summary enumeration mode.
+     * @throws RemoteException
+     */
+    void startTaggedSummaryEnumeration() throws RemoteException {
+        mSummary = mSession.getTaggedSummaryForAllUid(mTemplate, mStartTimeStamp, mEndTimeStamp);
+        mEnumerationIndex = 0;
+    }
+
+    /**
+     * Collects history results for uid and resets history enumeration index.
+     */
+    void startHistoryUidEnumeration(int uid, int tag, int state) {
+        mHistory = null;
+        try {
+            mHistory = mSession.getHistoryIntervalForUid(mTemplate, uid,
+                    Bucket.convertSet(state), tag, NetworkStatsHistory.FIELD_ALL,
+                    mStartTimeStamp, mEndTimeStamp);
+            setSingleUidTagState(uid, tag, state);
+        } catch (RemoteException e) {
+            Log.w(TAG, e);
+            // Leaving mHistory null
+        }
+        mEnumerationIndex = 0;
+    }
+
+    /**
+     * Collects history results for network and resets history enumeration index.
+     */
+    void startHistoryDeviceEnumeration() {
+        try {
+            mHistory = mSession.getHistoryIntervalForNetwork(
+                    mTemplate, NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+        } catch (RemoteException e) {
+            Log.w(TAG, e);
+            mHistory = null;
+        }
+        mEnumerationIndex = 0;
+    }
+
+    /**
+     * Starts uid enumeration for current user.
+     * @throws RemoteException
+     */
+    void startUserUidEnumeration() throws RemoteException {
+        // TODO: getRelevantUids should be sensitive to time interval. When that's done,
+        //       the filtering logic below can be removed.
+        int[] uids = mSession.getRelevantUids();
+        // Filtering of uids with empty history.
+        final ArrayList<Integer> filteredUids = new ArrayList<>();
+        for (int uid : uids) {
+            try {
+                NetworkStatsHistory history = mSession.getHistoryIntervalForUid(mTemplate, uid,
+                        android.net.NetworkStats.SET_ALL, android.net.NetworkStats.TAG_NONE,
+                        NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+                if (history != null && history.size() > 0) {
+                    filteredUids.add(uid);
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error while getting history of uid " + uid, e);
+            }
+        }
+        mUids = CollectionUtils.toIntArray(filteredUids);
+        mUidOrUidIndex = -1;
+        stepHistory();
+    }
+
+    /**
+     * Steps to next uid in enumeration and collects history for that.
+     */
+    private void stepHistory(){
+        if (hasNextUid()) {
+            stepUid();
+            mHistory = null;
+            try {
+                mHistory = mSession.getHistoryIntervalForUid(mTemplate, getUid(),
+                        android.net.NetworkStats.SET_ALL, android.net.NetworkStats.TAG_NONE,
+                        NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+            } catch (RemoteException e) {
+                Log.w(TAG, e);
+                // Leaving mHistory null
+            }
+            mEnumerationIndex = 0;
+        }
+    }
+
+    private void fillBucketFromSummaryEntry(Bucket bucketOut) {
+        bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid);
+        bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag);
+        bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set);
+        bucketOut.mDefaultNetworkStatus = Bucket.convertDefaultNetworkStatus(
+                mRecycledSummaryEntry.defaultNetwork);
+        bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered);
+        bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming);
+        bucketOut.mBeginTimeStamp = mStartTimeStamp;
+        bucketOut.mEndTimeStamp = mEndTimeStamp;
+        bucketOut.mRxBytes = mRecycledSummaryEntry.rxBytes;
+        bucketOut.mRxPackets = mRecycledSummaryEntry.rxPackets;
+        bucketOut.mTxBytes = mRecycledSummaryEntry.txBytes;
+        bucketOut.mTxPackets = mRecycledSummaryEntry.txPackets;
+    }
+
+    /**
+     * Getting the next item in summary enumeration.
+     * @param bucketOut Next item will be set here.
+     * @return true if a next item could be set.
+     */
+    private boolean getNextSummaryBucket(@Nullable Bucket bucketOut) {
+        if (bucketOut != null && mEnumerationIndex < mSummary.size()) {
+            mRecycledSummaryEntry = mSummary.getValues(mEnumerationIndex++, mRecycledSummaryEntry);
+            fillBucketFromSummaryEntry(bucketOut);
+            return true;
+        }
+        return false;
+    }
+
+    Bucket getSummaryAggregate() {
+        if (mSummary == null) {
+            return null;
+        }
+        Bucket bucket = new Bucket();
+        if (mRecycledSummaryEntry == null) {
+            mRecycledSummaryEntry = new android.net.NetworkStats.Entry();
+        }
+        mSummary.getTotal(mRecycledSummaryEntry);
+        fillBucketFromSummaryEntry(bucket);
+        return bucket;
+    }
+
+    /**
+     * Getting the next item in a history enumeration.
+     * @param bucketOut Next item will be set here.
+     * @return true if a next item could be set.
+     */
+    private boolean getNextHistoryBucket(@Nullable Bucket bucketOut) {
+        if (bucketOut != null && mHistory != null) {
+            if (mEnumerationIndex < mHistory.size()) {
+                mRecycledHistoryEntry = mHistory.getValues(mEnumerationIndex++,
+                        mRecycledHistoryEntry);
+                bucketOut.mUid = Bucket.convertUid(getUid());
+                bucketOut.mTag = Bucket.convertTag(mTag);
+                bucketOut.mState = mState;
+                bucketOut.mDefaultNetworkStatus = Bucket.DEFAULT_NETWORK_ALL;
+                bucketOut.mMetered = Bucket.METERED_ALL;
+                bucketOut.mRoaming = Bucket.ROAMING_ALL;
+                bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart;
+                bucketOut.mEndTimeStamp = mRecycledHistoryEntry.bucketStart +
+                        mRecycledHistoryEntry.bucketDuration;
+                bucketOut.mRxBytes = mRecycledHistoryEntry.rxBytes;
+                bucketOut.mRxPackets = mRecycledHistoryEntry.rxPackets;
+                bucketOut.mTxBytes = mRecycledHistoryEntry.txBytes;
+                bucketOut.mTxPackets = mRecycledHistoryEntry.txPackets;
+                return true;
+            } else if (hasNextUid()) {
+                stepHistory();
+                return getNextHistoryBucket(bucketOut);
+            }
+        }
+        return false;
+    }
+
+    // ------------------ UID LOGIC------------------------
+
+    private boolean isUidEnumeration() {
+        return mUids != null;
+    }
+
+    private boolean hasNextUid() {
+        return isUidEnumeration() && (mUidOrUidIndex + 1) < mUids.length;
+    }
+
+    private int getUid() {
+        // Check if uid enumeration.
+        if (isUidEnumeration()) {
+            if (mUidOrUidIndex < 0 || mUidOrUidIndex >= mUids.length) {
+                throw new IndexOutOfBoundsException(
+                        "Index=" + mUidOrUidIndex + " mUids.length=" + mUids.length);
+            }
+            return mUids[mUidOrUidIndex];
+        }
+        // Single uid mode.
+        return mUidOrUidIndex;
+    }
+
+    private void setSingleUidTagState(int uid, int tag, int state) {
+        mUidOrUidIndex = uid;
+        mTag = tag;
+        mState = state;
+    }
+
+    private void stepUid() {
+        if (mUids != null) {
+            ++mUidOrUidIndex;
+        }
+    }
+}
diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
new file mode 100644
index 0000000..f41475b
--- /dev/null
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -0,0 +1,1238 @@
+/**
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.WorkerThread;
+import android.app.usage.NetworkStats.Bucket;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.DataUsageRequest;
+import android.net.INetworkStatsService;
+import android.net.Network;
+import android.net.NetworkStack;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkTemplate;
+import android.net.UnderlyingNetworkInfo;
+import android.net.netstats.IUsageCallback;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.net.netstats.provider.NetworkStatsProvider;
+import android.os.Build;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.NetworkIdentityUtils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Provides access to network usage history and statistics. Usage data is collected in
+ * discrete bins of time called 'Buckets'. See {@link NetworkStats.Bucket} for details.
+ * <p />
+ * Queries can define a time interval in the form of start and end timestamps (Long.MIN_VALUE and
+ * Long.MAX_VALUE can be used to simulate open ended intervals). By default, apps can only obtain
+ * data about themselves. See the below note for special cases in which apps can obtain data about
+ * other applications.
+ * <h3>
+ * Summary queries
+ * </h3>
+ * {@link #querySummaryForDevice} <p />
+ * {@link #querySummaryForUser} <p />
+ * {@link #querySummary} <p />
+ * These queries aggregate network usage across the whole interval. Therefore there will be only one
+ * bucket for a particular key, state, metered and roaming combination. In case of the user-wide
+ * and device-wide summaries a single bucket containing the totalised network usage is returned.
+ * <h3>
+ * History queries
+ * </h3>
+ * {@link #queryDetailsForUid} <p />
+ * {@link #queryDetails} <p />
+ * These queries do not aggregate over time but do aggregate over state, metered and roaming.
+ * Therefore there can be multiple buckets for a particular key. However, all Buckets will have
+ * {@code state} {@link NetworkStats.Bucket#STATE_ALL},
+ * {@code defaultNetwork} {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * {@code metered } {@link NetworkStats.Bucket#METERED_ALL},
+ * {@code roaming} {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p />
+ * <b>NOTE:</b> Calling {@link #querySummaryForDevice} or accessing stats for apps other than the
+ * calling app requires the permission {@link android.Manifest.permission#PACKAGE_USAGE_STATS},
+ * which is a system-level permission and will not be granted to third-party apps. However,
+ * declaring the permission implies intention to use the API and the user of the device can grant
+ * permission through the Settings application.
+ * <p />
+ * Profile owner apps are automatically granted permission to query data on the profile they manage
+ * (that is, for any query except {@link #querySummaryForDevice}). Device owner apps and carrier-
+ * privileged apps likewise get access to usage data for all users on the device.
+ * <p />
+ * In addition to tethering usage, usage by removed users and apps, and usage by the system
+ * is also included in the results for callers with one of these higher levels of access.
+ * <p />
+ * <b>NOTE:</b> Prior to API level {@value android.os.Build.VERSION_CODES#N}, all calls to these APIs required
+ * the above permission, even to access an app's own data usage, and carrier-privileged apps were
+ * not included.
+ */
+@SystemService(Context.NETWORK_STATS_SERVICE)
+public class NetworkStatsManager {
+    private static final String TAG = "NetworkStatsManager";
+    private static final boolean DBG = false;
+
+    /** @hide */
+    public static final int CALLBACK_LIMIT_REACHED = 0;
+    /** @hide */
+    public static final int CALLBACK_RELEASED = 1;
+
+    /**
+     * Minimum data usage threshold for registering usage callbacks.
+     *
+     * Requests registered with a threshold lower than this will only be triggered once this minimum
+     * is reached.
+     * @hide
+     */
+    public static final long MIN_THRESHOLD_BYTES = 2 * 1_048_576L; // 2MiB
+
+    private final Context mContext;
+    private final INetworkStatsService mService;
+
+    /**
+     * @deprecated Use {@link NetworkStatsDataMigrationUtils#PREFIX_XT}
+     * instead.
+     * @hide
+     */
+    @Deprecated
+    public static final String PREFIX_DEV = "dev";
+
+    /** @hide */
+    public static final int FLAG_POLL_ON_OPEN = 1 << 0;
+    /** @hide */
+    public static final int FLAG_POLL_FORCE = 1 << 1;
+    /** @hide */
+    public static final int FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN = 1 << 2;
+
+    /**
+     * Virtual RAT type to represent 5G NSA (Non Stand Alone) mode, where the primary cell is
+     * still LTE and network allocates a secondary 5G cell so telephony reports RAT = LTE along
+     * with NR state as connected. This is a concept added by NetworkStats on top of the telephony
+     * constants for backward compatibility of metrics so this should not be overlapped with any of
+     * the {@code TelephonyManager.NETWORK_TYPE_*} constants.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int NETWORK_TYPE_5G_NSA = -2;
+
+    private int mFlags;
+
+    /** @hide */
+    @VisibleForTesting
+    public NetworkStatsManager(Context context, INetworkStatsService service) {
+        mContext = context;
+        mService = service;
+        setPollOnOpen(true);
+        setAugmentWithSubscriptionPlan(true);
+    }
+
+    /** @hide */
+    public INetworkStatsService getBinder() {
+        return mService;
+    }
+
+    /**
+     * Set poll on open flag to indicate the poll is needed before service gets statistics
+     * result. This is default enabled. However, for any non-privileged caller, the poll might
+     * be omitted in case of rate limiting.
+     *
+     * @param pollOnOpen true if poll is needed.
+     * @hide
+     */
+    // The system will ignore any non-default values for non-privileged
+    // processes, so processes that don't hold the appropriate permissions
+    // can make no use of this API.
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    public void setPollOnOpen(boolean pollOnOpen) {
+        if (pollOnOpen) {
+            mFlags |= FLAG_POLL_ON_OPEN;
+        } else {
+            mFlags &= ~FLAG_POLL_ON_OPEN;
+        }
+    }
+
+    /**
+     * Set poll force flag to indicate that calling any subsequent query method will force a stats
+     * poll.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void setPollForce(boolean pollForce) {
+        if (pollForce) {
+            mFlags |= FLAG_POLL_FORCE;
+        } else {
+            mFlags &= ~FLAG_POLL_FORCE;
+        }
+    }
+
+    /** @hide */
+    public void setAugmentWithSubscriptionPlan(boolean augmentWithSubscriptionPlan) {
+        if (augmentWithSubscriptionPlan) {
+            mFlags |= FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN;
+        } else {
+            mFlags &= ~FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN;
+        }
+    }
+
+    /**
+     * Query network usage statistics summaries.
+     *
+     * Result is summarised data usage for the whole
+     * device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and
+     * roaming. This means the bucket's start and end timestamp will be the same as the
+     * 'startTime' and 'endTime' arguments. State is going to be
+     * {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL},
+     * tag {@link NetworkStats.Bucket#TAG_NONE},
+     * default network {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+     * metered {@link NetworkStats.Bucket#METERED_ALL},
+     * and roaming {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param template Template used to match networks. See {@link NetworkTemplate}.
+     * @param startTime Start of period, in milliseconds since the Unix epoch, see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period, in milliseconds since the Unix epoch, see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @return Bucket Summarised data usage.
+     *
+     * @hide
+     */
+    @NonNull
+    @WorkerThread
+    @SystemApi(client = MODULE_LIBRARIES)
+    public Bucket querySummaryForDevice(@NonNull NetworkTemplate template,
+            long startTime, long endTime) {
+        Objects.requireNonNull(template);
+        try {
+            NetworkStats stats =
+                    new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+            Bucket bucket = stats.getDeviceSummaryForNetwork();
+            stats.close();
+            return bucket;
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        return null; // To make the compiler happy.
+    }
+
+    /**
+     * Query network usage statistics summaries. Result is summarised data usage for the whole
+     * device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and
+     * roaming. This means the bucket's start and end timestamp are going to be the same as the
+     * 'startTime' and 'endTime' parameters. State is going to be
+     * {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL},
+     * tag {@link NetworkStats.Bucket#TAG_NONE},
+     * default network {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+     * metered {@link NetworkStats.Bucket#METERED_ALL},
+     * and roaming {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @return Bucket object or null if permissions are insufficient or error happened during
+     *         statistics collection.
+     */
+    @WorkerThread
+    public Bucket querySummaryForDevice(int networkType, @Nullable String subscriberId,
+            long startTime, long endTime) throws SecurityException, RemoteException {
+        NetworkTemplate template;
+        try {
+            template = createTemplate(networkType, subscriberId);
+        } catch (IllegalArgumentException e) {
+            if (DBG) Log.e(TAG, "Cannot create template", e);
+            return null;
+        }
+
+        return querySummaryForDevice(template, startTime, endTime);
+    }
+
+    /**
+     * Query network usage statistics summaries. Result is summarised data usage for all uids
+     * belonging to calling user. Result is a single Bucket aggregated over time, state and uid.
+     * This means the bucket's start and end timestamp are going to be the same as the 'startTime'
+     * and 'endTime' parameters. State is going to be {@link NetworkStats.Bucket#STATE_ALL},
+     * uid {@link NetworkStats.Bucket#UID_ALL}, tag {@link NetworkStats.Bucket#TAG_NONE},
+     * metered {@link NetworkStats.Bucket#METERED_ALL}, and roaming
+     * {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @return Bucket object or null if permissions are insufficient or error happened during
+     *         statistics collection.
+     */
+    @WorkerThread
+    public Bucket querySummaryForUser(int networkType, @Nullable String subscriberId,
+            long startTime, long endTime) throws SecurityException, RemoteException {
+        NetworkTemplate template;
+        try {
+            template = createTemplate(networkType, subscriberId);
+        } catch (IllegalArgumentException e) {
+            if (DBG) Log.e(TAG, "Cannot create template", e);
+            return null;
+        }
+
+        NetworkStats stats;
+        stats = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+        stats.startSummaryEnumeration();
+
+        stats.close();
+        return stats.getSummaryAggregate();
+    }
+
+    /**
+     * Query network usage statistics summaries. Result filtered to include only uids belonging to
+     * calling user. Result is aggregated over time, hence all buckets will have the same start and
+     * end timestamps. Not aggregated over state, uid, default network, metered, or roaming. This
+     * means buckets' start and end timestamps are going to be the same as the 'startTime' and
+     * 'endTime' parameters. State, uid, metered, and roaming are going to vary, and tag is going to
+     * be the same.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @return Statistics object or null if permissions are insufficient or error happened during
+     *         statistics collection.
+     */
+    @WorkerThread
+    public NetworkStats querySummary(int networkType, @Nullable String subscriberId, long startTime,
+            long endTime) throws SecurityException, RemoteException {
+        NetworkTemplate template;
+        try {
+            template = createTemplate(networkType, subscriberId);
+        } catch (IllegalArgumentException e) {
+            if (DBG) Log.e(TAG, "Cannot create template", e);
+            return null;
+        }
+
+        return querySummary(template, startTime, endTime);
+    }
+
+    /**
+     * Query network usage statistics summaries.
+     *
+     * The results will only include traffic made by UIDs belonging to the calling user profile.
+     * The results are aggregated over time, so that all buckets will have the same start and
+     * end timestamps as the passed arguments. Not aggregated over state, uid, default network,
+     * metered, or roaming.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param template Template used to match networks. See {@link NetworkTemplate}.
+     * @param startTime Start of period, in milliseconds since the Unix epoch, see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period, in milliseconds since the Unix epoch, see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @return Statistics which is described above.
+     * @hide
+     */
+    @NonNull
+    @SystemApi(client = MODULE_LIBRARIES)
+    @WorkerThread
+    public NetworkStats querySummary(@NonNull NetworkTemplate template, long startTime,
+            long endTime) throws SecurityException {
+        Objects.requireNonNull(template);
+        try {
+            NetworkStats result =
+                    new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+            result.startSummaryEnumeration();
+            return result;
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        return null; // To make the compiler happy.
+    }
+
+    /**
+     * Query tagged network usage statistics summaries.
+     *
+     * The results will only include tagged traffic made by UIDs belonging to the calling user
+     * profile. The results are aggregated over time, so that all buckets will have the same
+     * start and end timestamps as the passed arguments. Not aggregated over state, uid,
+     * default network, metered, or roaming.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param template Template used to match networks. See {@link NetworkTemplate}.
+     * @param startTime Start of period, in milliseconds since the Unix epoch, see
+     *            {@link System#currentTimeMillis}.
+     * @param endTime End of period, in milliseconds since the Unix epoch, see
+     *            {@link System#currentTimeMillis}.
+     * @return Statistics which is described above.
+     * @hide
+     */
+    @NonNull
+    @SystemApi(client = MODULE_LIBRARIES)
+    @WorkerThread
+    public NetworkStats queryTaggedSummary(@NonNull NetworkTemplate template, long startTime,
+            long endTime) throws SecurityException {
+        Objects.requireNonNull(template);
+        try {
+            NetworkStats result =
+                    new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+            result.startTaggedSummaryEnumeration();
+            return result;
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        return null; // To make the compiler happy.
+    }
+
+    /**
+     * Query usage statistics details for networks matching a given {@link NetworkTemplate}.
+     *
+     * Result is not aggregated over time. This means buckets' start and
+     * end timestamps will be between 'startTime' and 'endTime' parameters.
+     * <p>Only includes buckets whose entire time period is included between
+     * startTime and endTime. Doesn't interpolate or return partial buckets.
+     * Since bucket length is in the order of hours, this
+     * method cannot be used to measure data usage on a fine grained time scale.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param template Template used to match networks. See {@link NetworkTemplate}.
+     * @param startTime Start of period, in milliseconds since the Unix epoch, see
+     *                  {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period, in milliseconds since the Unix epoch, see
+     *                {@link java.lang.System#currentTimeMillis}.
+     * @return Statistics which is described above.
+     * @hide
+     */
+    @NonNull
+    @SystemApi(client = MODULE_LIBRARIES)
+    @WorkerThread
+    public NetworkStats queryDetailsForDevice(@NonNull NetworkTemplate template,
+            long startTime, long endTime) {
+        Objects.requireNonNull(template);
+        try {
+            final NetworkStats result =
+                    new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+            result.startHistoryDeviceEnumeration();
+            return result;
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+
+        return null; // To make the compiler happy.
+    }
+
+    /**
+     * Query network usage statistics details for a given uid.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
+     */
+    @NonNull
+    @WorkerThread
+    public NetworkStats queryDetailsForUid(int networkType, @Nullable String subscriberId,
+            long startTime, long endTime, int uid) throws SecurityException {
+        return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
+            NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
+    }
+
+    /** @hide */
+    @NonNull
+    public NetworkStats queryDetailsForUid(@NonNull NetworkTemplate template,
+            long startTime, long endTime, int uid) throws SecurityException {
+        return queryDetailsForUidTagState(template, startTime, endTime, uid,
+                NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
+    }
+
+    /**
+     * Query network usage statistics details for a given uid and tag.
+     *
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     * Only usable for uids belonging to calling user. Result is not aggregated over time.
+     * This means buckets' start and end timestamps are going to be between 'startTime' and
+     * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+     * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+     * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+     * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+     * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+     * interpolate across partial buckets. Since bucket length is in the order of hours, this
+     * method cannot be used to measure data usage on a fine grained time scale.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param uid UID of app
+     * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+     *            across all the tags.
+     * @return Statistics which is described above.
+     * @throws SecurityException if permissions are insufficient to read network statistics.
+     */
+    @NonNull
+    @WorkerThread
+    public NetworkStats queryDetailsForUidTag(int networkType, @Nullable String subscriberId,
+            long startTime, long endTime, int uid, int tag) throws SecurityException {
+        return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
+            tag, NetworkStats.Bucket.STATE_ALL);
+    }
+
+    /**
+     * Query network usage statistics details for a given uid, tag, and state.
+     *
+     * Only usable for uids belonging to calling user. Result is not aggregated over time.
+     * This means buckets' start and end timestamps are going to be between 'startTime' and
+     * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+     * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+     * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+     * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+     * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+     * interpolate across partial buckets. Since bucket length is in the order of hours, this
+     * method cannot be used to measure data usage on a fine grained time scale.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param uid UID of app
+     * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+     *            across all the tags.
+     * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
+     *            traffic from all states.
+     * @return Statistics which is described above.
+     * @throws SecurityException if permissions are insufficient to read network statistics.
+     */
+    @NonNull
+    @WorkerThread
+    public NetworkStats queryDetailsForUidTagState(int networkType, @Nullable String subscriberId,
+            long startTime, long endTime, int uid, int tag, int state) throws SecurityException {
+        NetworkTemplate template;
+        template = createTemplate(networkType, subscriberId);
+
+        return queryDetailsForUidTagState(template, startTime, endTime, uid, tag, state);
+    }
+
+    /**
+     * Query network usage statistics details for a given template, uid, tag, and state.
+     *
+     * Only usable for uids belonging to calling user. Result is not aggregated over time.
+     * This means buckets' start and end timestamps are going to be between 'startTime' and
+     * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+     * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+     * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+     * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+     * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+     * interpolate across partial buckets. Since bucket length is in the order of hours, this
+     * method cannot be used to measure data usage on a fine grained time scale.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param template Template used to match networks. See {@link NetworkTemplate}.
+     * @param startTime Start of period, in milliseconds since the Unix epoch, see
+     *                  {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period, in milliseconds since the Unix epoch, see
+     *                {@link java.lang.System#currentTimeMillis}.
+     * @param uid UID of app
+     * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+     *            across all the tags.
+     * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
+     *            traffic from all states.
+     * @return Statistics which is described above.
+     * @hide
+     */
+    @NonNull
+    @SystemApi(client = MODULE_LIBRARIES)
+    @WorkerThread
+    public NetworkStats queryDetailsForUidTagState(@NonNull NetworkTemplate template,
+            long startTime, long endTime, int uid, int tag, int state) throws SecurityException {
+        Objects.requireNonNull(template);
+        try {
+            final NetworkStats result = new NetworkStats(
+                    mContext, template, mFlags, startTime, endTime, mService);
+            result.startHistoryUidEnumeration(uid, tag, state);
+            return result;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error while querying stats for uid=" + uid + " tag=" + tag
+                    + " state=" + state, e);
+            e.rethrowFromSystemServer();
+        }
+
+        return null; // To make the compiler happy.
+    }
+
+    /**
+     * Query network usage statistics details. Result filtered to include only uids belonging to
+     * calling user. Result is aggregated over state but not aggregated over time, uid, tag,
+     * metered, nor roaming. This means buckets' start and end timestamps are going to be between
+     * 'startTime' and 'endTime' parameters. State is going to be
+     * {@link NetworkStats.Bucket#STATE_ALL}, uid will vary,
+     * tag {@link NetworkStats.Bucket#TAG_NONE},
+     * default network is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+     * metered is going to be {@link NetworkStats.Bucket#METERED_ALL},
+     * and roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+     * interpolate across partial buckets. Since bucket length is in the order of hours, this
+     * method cannot be used to measure data usage on a fine grained time scale.
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     *
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @return Statistics object or null if permissions are insufficient or error happened during
+     *         statistics collection.
+     */
+    @WorkerThread
+    public NetworkStats queryDetails(int networkType, @Nullable String subscriberId, long startTime,
+            long endTime) throws SecurityException, RemoteException {
+        NetworkTemplate template;
+        try {
+            template = createTemplate(networkType, subscriberId);
+        } catch (IllegalArgumentException e) {
+            if (DBG) Log.e(TAG, "Cannot create template", e);
+            return null;
+        }
+
+        NetworkStats result;
+        result = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+        result.startUserUidEnumeration();
+        return result;
+    }
+
+    /**
+     * Query realtime mobile network usage statistics.
+     *
+     * Return a snapshot of current UID network statistics, as it applies
+     * to the mobile radios of the device. The snapshot will include any
+     * tethering traffic, video calling data usage and count of
+     * network operations set by {@link TrafficStats#incrementOperationCount}
+     * made over a mobile radio.
+     * The snapshot will not include any statistics that cannot be seen by
+     * the kernel, e.g. statistics reported by {@link NetworkStatsProvider}s.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    @NonNull public android.net.NetworkStats getMobileUidStats() {
+        try {
+            return mService.getUidStatsForTransport(TRANSPORT_CELLULAR);
+        } catch (RemoteException e) {
+            if (DBG) Log.d(TAG, "Remote exception when get Mobile uid stats");
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Query realtime Wi-Fi network usage statistics.
+     *
+     * Return a snapshot of current UID network statistics, as it applies
+     * to the Wi-Fi radios of the device. The snapshot will include any
+     * tethering traffic, video calling data usage and count of
+     * network operations set by {@link TrafficStats#incrementOperationCount}
+     * made over a Wi-Fi radio.
+     * The snapshot will not include any statistics that cannot be seen by
+     * the kernel, e.g. statistics reported by {@link NetworkStatsProvider}s.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    @NonNull public android.net.NetworkStats getWifiUidStats() {
+        try {
+            return mService.getUidStatsForTransport(TRANSPORT_WIFI);
+        } catch (RemoteException e) {
+            if (DBG) Log.d(TAG, "Remote exception when get WiFi uid stats");
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers to receive notifications about data usage on specified networks.
+     *
+     * <p>The callbacks will continue to be called as long as the process is alive or
+     * {@link #unregisterUsageCallback} is called.
+     *
+     * @param template Template used to match networks. See {@link NetworkTemplate}.
+     * @param thresholdBytes Threshold in bytes to be notified on. Provided values lower than 2MiB
+     *                       will be clamped for callers except callers with the NETWORK_STACK
+     *                       permission.
+     * @param executor The executor on which callback will be invoked. The provided {@link Executor}
+     *                 must run callback sequentially, otherwise the order of callbacks cannot be
+     *                 guaranteed.
+     * @param callback The {@link UsageCallback} that the system will call when data usage
+     *                 has exceeded the specified threshold.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK}, conditional = true)
+    public void registerUsageCallback(@NonNull NetworkTemplate template, long thresholdBytes,
+            @NonNull @CallbackExecutor Executor executor, @NonNull UsageCallback callback) {
+        Objects.requireNonNull(template, "NetworkTemplate cannot be null");
+        Objects.requireNonNull(callback, "UsageCallback cannot be null");
+        Objects.requireNonNull(executor, "Executor cannot be null");
+
+        final DataUsageRequest request = new DataUsageRequest(DataUsageRequest.REQUEST_ID_UNSET,
+                template, thresholdBytes);
+        try {
+            final UsageCallbackWrapper callbackWrapper =
+                    new UsageCallbackWrapper(executor, callback);
+            callback.request = mService.registerUsageCallback(
+                    mContext.getOpPackageName(), request, callbackWrapper);
+            if (DBG) Log.d(TAG, "registerUsageCallback returned " + callback.request);
+
+            if (callback.request == null) {
+                Log.e(TAG, "Request from callback is null; should not happen");
+            }
+        } catch (RemoteException e) {
+            if (DBG) Log.d(TAG, "Remote exception when registering callback");
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers to receive notifications about data usage on specified networks.
+     *
+     * <p>The callbacks will continue to be called as long as the process is live or
+     * {@link #unregisterUsageCallback} is called.
+     *
+     * @param networkType Type of network to monitor. Either
+    {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when registering for the mobile network type to receive
+     *                     notifications for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param thresholdBytes Threshold in bytes to be notified on.
+     * @param callback The {@link UsageCallback} that the system will call when data usage
+     *            has exceeded the specified threshold.
+     */
+    public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+            long thresholdBytes, @NonNull UsageCallback callback) {
+        registerUsageCallback(networkType, subscriberId, thresholdBytes, callback,
+                null /* handler */);
+    }
+
+    /**
+     * Registers to receive notifications about data usage on specified networks.
+     *
+     * <p>The callbacks will continue to be called as long as the process is live or
+     * {@link #unregisterUsageCallback} is called.
+     *
+     * @param networkType Type of network to monitor. Either
+                  {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when registering for the mobile network type to receive
+     *                     notifications for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param thresholdBytes Threshold in bytes to be notified on.
+     * @param callback The {@link UsageCallback} that the system will call when data usage
+     *            has exceeded the specified threshold.
+     * @param handler to dispatch callback events through, otherwise if {@code null} it uses
+     *            the calling thread.
+     */
+    public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+            long thresholdBytes, @NonNull UsageCallback callback, @Nullable Handler handler) {
+        NetworkTemplate template = createTemplate(networkType, subscriberId);
+        if (DBG) {
+            Log.d(TAG, "registerUsageCallback called with: {"
+                    + " networkType=" + networkType
+                    + " subscriberId=" + subscriberId
+                    + " thresholdBytes=" + thresholdBytes
+                    + " }");
+        }
+
+        final Executor executor = handler == null ? r -> r.run() : r -> handler.post(r);
+
+        registerUsageCallback(template, thresholdBytes, executor, callback);
+    }
+
+    /**
+     * Unregisters callbacks on data usage.
+     *
+     * @param callback The {@link UsageCallback} used when registering.
+     */
+    public void unregisterUsageCallback(@NonNull UsageCallback callback) {
+        if (callback == null || callback.request == null
+                || callback.request.requestId == DataUsageRequest.REQUEST_ID_UNSET) {
+            throw new IllegalArgumentException("Invalid UsageCallback");
+        }
+        try {
+            mService.unregisterUsageRequest(callback.request);
+        } catch (RemoteException e) {
+            if (DBG) Log.d(TAG, "Remote exception when unregistering callback");
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Base class for usage callbacks. Should be extended by applications wanting notifications.
+     */
+    public static abstract class UsageCallback {
+        /**
+         * Called when data usage has reached the given threshold.
+         *
+         * Called by {@code NetworkStatsService} when the registered threshold is reached.
+         * If a caller implements {@link #onThresholdReached(NetworkTemplate)}, the system
+         * will not call {@link #onThresholdReached(int, String)}.
+         *
+         * @param template The {@link NetworkTemplate} that associated with this callback.
+         * @hide
+         */
+        @SystemApi(client = MODULE_LIBRARIES)
+        public void onThresholdReached(@NonNull NetworkTemplate template) {
+            // Backward compatibility for those who didn't override this function.
+            final int networkType = networkTypeForTemplate(template);
+            if (networkType != ConnectivityManager.TYPE_NONE) {
+                final String subscriberId = template.getSubscriberIds().isEmpty() ? null
+                        : template.getSubscriberIds().iterator().next();
+                onThresholdReached(networkType, subscriberId);
+            }
+        }
+
+        /**
+         * Called when data usage has reached the given threshold.
+         */
+        public abstract void onThresholdReached(int networkType, @Nullable String subscriberId);
+
+        /**
+         * @hide used for internal bookkeeping
+         */
+        private DataUsageRequest request;
+
+        /**
+         * Get network type from a template if feasible.
+         *
+         * @param template the target {@link NetworkTemplate}.
+         * @return legacy network type, only supports for the types which is already supported in
+         *         {@link #registerUsageCallback(int, String, long, UsageCallback, Handler)}.
+         *         {@link ConnectivityManager#TYPE_NONE} for other types.
+         */
+        private static int networkTypeForTemplate(@NonNull NetworkTemplate template) {
+            switch (template.getMatchRule()) {
+                case NetworkTemplate.MATCH_MOBILE:
+                    return ConnectivityManager.TYPE_MOBILE;
+                case NetworkTemplate.MATCH_WIFI:
+                    return ConnectivityManager.TYPE_WIFI;
+                default:
+                    return ConnectivityManager.TYPE_NONE;
+            }
+        }
+    }
+
+    /**
+     * Registers a custom provider of {@link android.net.NetworkStats} to provide network statistics
+     * to the system. To unregister, invoke {@link #unregisterNetworkStatsProvider}.
+     * Note that no de-duplication of statistics between providers is performed, so each provider
+     * must only report network traffic that is not being reported by any other provider. Also note
+     * that the provider cannot be re-registered after unregistering.
+     *
+     * @param tag a human readable identifier of the custom network stats provider. This is only
+     *            used for debugging.
+     * @param provider the subclass of {@link NetworkStatsProvider} that needs to be
+     *                 registered to the system.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_STATS_PROVIDER,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
+    public void registerNetworkStatsProvider(
+            @NonNull String tag,
+            @NonNull NetworkStatsProvider provider) {
+        try {
+            if (provider.getProviderCallbackBinder() != null) {
+                throw new IllegalArgumentException("provider is already registered");
+            }
+            final INetworkStatsProviderCallback cbBinder =
+                    mService.registerNetworkStatsProvider(tag, provider.getProviderBinder());
+            provider.setProviderCallbackBinder(cbBinder);
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Unregisters an instance of {@link NetworkStatsProvider}.
+     *
+     * @param provider the subclass of {@link NetworkStatsProvider} that needs to be
+     *                 unregistered to the system.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_STATS_PROVIDER,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
+    public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) {
+        try {
+            provider.getProviderCallbackBinderOrThrow().unregister();
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    private static NetworkTemplate createTemplate(int networkType, @Nullable String subscriberId) {
+        final NetworkTemplate template;
+        switch (networkType) {
+            case ConnectivityManager.TYPE_MOBILE:
+                template = subscriberId == null
+                        ? NetworkTemplate.buildTemplateMobileWildcard()
+                        : NetworkTemplate.buildTemplateMobileAll(subscriberId);
+                break;
+            case ConnectivityManager.TYPE_WIFI:
+                template = TextUtils.isEmpty(subscriberId)
+                        ? NetworkTemplate.buildTemplateWifiWildcard()
+                        : NetworkTemplate.buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL,
+                                subscriberId);
+                break;
+            default:
+                throw new IllegalArgumentException("Cannot create template for network type "
+                        + networkType + ", subscriberId '"
+                        + NetworkIdentityUtils.scrubSubscriberId(subscriberId) + "'.");
+        }
+        return template;
+    }
+
+    /**
+     * Notify {@code NetworkStatsService} about network status changed.
+     *
+     * Notifies NetworkStatsService of network state changes for data usage accounting purposes.
+     *
+     * To avoid races that attribute data usage to wrong network, such as new network with
+     * the same interface after SIM hot-swap, this function will not return until
+     * {@code NetworkStatsService} finishes its work of retrieving traffic statistics from
+     * all data sources.
+     *
+     * @param defaultNetworks the list of all networks that could be used by network traffic that
+     *                        does not explicitly select a network.
+     * @param networkStateSnapshots a list of {@link NetworkStateSnapshot}s, one for
+     *                              each network that is currently connected.
+     * @param activeIface the active (i.e., connected) default network interface for the calling
+     *                    uid. Used to determine on which network future calls to
+     *                    {@link android.net.TrafficStats#incrementOperationCount} applies to.
+     * @param underlyingNetworkInfos the list of underlying network information for all
+     *                               currently-connected VPNs.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    public void notifyNetworkStatus(
+            @NonNull List<Network> defaultNetworks,
+            @NonNull List<NetworkStateSnapshot> networkStateSnapshots,
+            @Nullable String activeIface,
+            @NonNull List<UnderlyingNetworkInfo> underlyingNetworkInfos) {
+        try {
+            Objects.requireNonNull(defaultNetworks);
+            Objects.requireNonNull(networkStateSnapshots);
+            Objects.requireNonNull(underlyingNetworkInfos);
+            mService.notifyNetworkStatus(defaultNetworks.toArray(new Network[0]),
+                    networkStateSnapshots.toArray(new NetworkStateSnapshot[0]), activeIface,
+                    underlyingNetworkInfos.toArray(new UnderlyingNetworkInfo[0]));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private static class UsageCallbackWrapper extends IUsageCallback.Stub {
+        // Null if unregistered.
+        private volatile UsageCallback mCallback;
+
+        private final Executor mExecutor;
+
+        UsageCallbackWrapper(@NonNull Executor executor, @NonNull UsageCallback callback) {
+            mCallback = callback;
+            mExecutor = executor;
+        }
+
+        @Override
+        public void onThresholdReached(DataUsageRequest request) {
+            // Copy it to a local variable in case mCallback changed inside the if condition.
+            final UsageCallback callback = mCallback;
+            if (callback != null) {
+                mExecutor.execute(() -> callback.onThresholdReached(request.template));
+            } else {
+                Log.e(TAG, "onThresholdReached with released callback for " + request);
+            }
+        }
+
+        @Override
+        public void onCallbackReleased(DataUsageRequest request) {
+            if (DBG) Log.d(TAG, "callback released for " + request);
+            mCallback = null;
+        }
+    }
+
+    /**
+     * Mark given UID as being in foreground for stats purposes.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    public void noteUidForeground(int uid, boolean uidForeground) {
+        try {
+            mService.noteUidForeground(uid, uidForeground);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set default value of global alert bytes, the value will be clamped to [128kB, 2MB].
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            Manifest.permission.NETWORK_STACK})
+    public void setDefaultGlobalAlert(long alertBytes) {
+        try {
+            // TODO: Sync internal naming with the API surface.
+            mService.advisePersistThreshold(alertBytes);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Force update of statistics.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    public void forceUpdate() {
+        try {
+            mService.forceUpdate();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set the warning and limit to all registered custom network stats providers.
+     * Note that invocation of any interface will be sent to all providers.
+     *
+     * Asynchronicity notes : because traffic may be happening on the device at the same time, it
+     * doesn't make sense to wait for the warning and limit to be set – a caller still wouldn't
+     * know when exactly it was effective. All that can matter is that it's done quickly. Also,
+     * this method can't fail, so there is no status to return. All providers will see the new
+     * values soon.
+     * As such, this method returns immediately and sends the warning and limit to all providers
+     * as soon as possible through a one-way binder call.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    public void setStatsProviderWarningAndLimitAsync(@NonNull String iface, long warning,
+            long limit) {
+        try {
+            mService.setStatsProviderWarningAndLimitAsync(iface, warning, limit);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get a RAT type representative of a group of RAT types for network statistics.
+     *
+     * Collapse the given Radio Access Technology (RAT) type into a bucket that
+     * is representative of the original RAT type for network statistics. The
+     * mapping mostly corresponds to {@code TelephonyManager#NETWORK_CLASS_BIT_MASK_*}
+     * but with adaptations specific to the virtual types introduced by
+     * networks stats.
+     *
+     * @param ratType An integer defined in {@code TelephonyManager#NETWORK_TYPE_*}.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static int getCollapsedRatType(int ratType) {
+        switch (ratType) {
+            case TelephonyManager.NETWORK_TYPE_GPRS:
+            case TelephonyManager.NETWORK_TYPE_GSM:
+            case TelephonyManager.NETWORK_TYPE_EDGE:
+            case TelephonyManager.NETWORK_TYPE_IDEN:
+            case TelephonyManager.NETWORK_TYPE_CDMA:
+            case TelephonyManager.NETWORK_TYPE_1xRTT:
+                return TelephonyManager.NETWORK_TYPE_GSM;
+            case TelephonyManager.NETWORK_TYPE_EVDO_0:
+            case TelephonyManager.NETWORK_TYPE_EVDO_A:
+            case TelephonyManager.NETWORK_TYPE_EVDO_B:
+            case TelephonyManager.NETWORK_TYPE_EHRPD:
+            case TelephonyManager.NETWORK_TYPE_UMTS:
+            case TelephonyManager.NETWORK_TYPE_HSDPA:
+            case TelephonyManager.NETWORK_TYPE_HSUPA:
+            case TelephonyManager.NETWORK_TYPE_HSPA:
+            case TelephonyManager.NETWORK_TYPE_HSPAP:
+            case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
+                return TelephonyManager.NETWORK_TYPE_UMTS;
+            case TelephonyManager.NETWORK_TYPE_LTE:
+            case TelephonyManager.NETWORK_TYPE_IWLAN:
+                return TelephonyManager.NETWORK_TYPE_LTE;
+            case TelephonyManager.NETWORK_TYPE_NR:
+                return TelephonyManager.NETWORK_TYPE_NR;
+            // Virtual RAT type for 5G NSA mode, see
+            // {@link NetworkStatsManager#NETWORK_TYPE_5G_NSA}.
+            case NetworkStatsManager.NETWORK_TYPE_5G_NSA:
+                return NetworkStatsManager.NETWORK_TYPE_5G_NSA;
+            default:
+                return TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        }
+    }
+}
diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
new file mode 100644
index 0000000..d9c9d74
--- /dev/null
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
+import android.net.mdns.aidl.IMDns;
+import android.net.nsd.INsdManager;
+import android.net.nsd.MDnsManager;
+import android.net.nsd.NsdManager;
+
+/**
+ * Class for performing registration for Connectivity services which are exposed via updatable APIs
+ * since Android T.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class ConnectivityFrameworkInitializerTiramisu {
+    private ConnectivityFrameworkInitializerTiramisu() {}
+
+    /**
+     * Called by {@link SystemServiceRegistry}'s static initializer and registers NetworkStats, nsd,
+     * ipsec and ethernet services to {@link Context}, so that {@link Context#getSystemService} can
+     * return them.
+     *
+     * @throws IllegalStateException if this is called anywhere besides
+     * {@link SystemServiceRegistry}.
+     */
+    public static void registerServiceWrappers() {
+        SystemServiceRegistry.registerContextAwareService(
+                Context.NSD_SERVICE,
+                NsdManager.class,
+                (context, serviceBinder) -> {
+                    INsdManager service = INsdManager.Stub.asInterface(serviceBinder);
+                    return new NsdManager(context, service);
+                }
+        );
+
+        SystemServiceRegistry.registerContextAwareService(
+                Context.IPSEC_SERVICE,
+                IpSecManager.class,
+                (context, serviceBinder) -> {
+                    IIpSecService service = IIpSecService.Stub.asInterface(serviceBinder);
+                    return new IpSecManager(context, service);
+                }
+        );
+
+        SystemServiceRegistry.registerContextAwareService(
+                Context.NETWORK_STATS_SERVICE,
+                NetworkStatsManager.class,
+                (context, serviceBinder) -> {
+                    INetworkStatsService service =
+                            INetworkStatsService.Stub.asInterface(serviceBinder);
+                    return new NetworkStatsManager(context, service);
+                }
+        );
+
+        SystemServiceRegistry.registerContextAwareService(
+                Context.ETHERNET_SERVICE,
+                EthernetManager.class,
+                (context, serviceBinder) -> {
+                    IEthernetManager service = IEthernetManager.Stub.asInterface(serviceBinder);
+                    return new EthernetManager(context, service);
+                }
+        );
+
+        SystemServiceRegistry.registerStaticService(
+                MDnsManager.MDNS_SERVICE,
+                MDnsManager.class,
+                (serviceBinder) -> {
+                    IMDns service = IMDns.Stub.asInterface(serviceBinder);
+                    return new MDnsManager(service);
+                }
+        );
+    }
+}
diff --git a/framework-t/src/android/net/DataUsageRequest.aidl b/framework-t/src/android/net/DataUsageRequest.aidl
new file mode 100644
index 0000000..d1937c7
--- /dev/null
+++ b/framework-t/src/android/net/DataUsageRequest.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable DataUsageRequest;
diff --git a/framework-t/src/android/net/DataUsageRequest.java b/framework-t/src/android/net/DataUsageRequest.java
new file mode 100644
index 0000000..b06d515
--- /dev/null
+++ b/framework-t/src/android/net/DataUsageRequest.java
@@ -0,0 +1,112 @@
+/**
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.net;
+
+import android.annotation.Nullable;
+import android.net.NetworkTemplate;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Defines a request to register a callbacks. Used to be notified on data usage via
+ * {@link android.app.usage.NetworkStatsManager#registerDataUsageCallback}.
+ * If no {@code uid}s are set, callbacks are restricted to device-owners,
+ * carrier-privileged apps, or system apps.
+ *
+ * @hide
+ */
+public final class DataUsageRequest implements Parcelable {
+
+    public static final String PARCELABLE_KEY = "DataUsageRequest";
+    public static final int REQUEST_ID_UNSET = 0;
+
+    /**
+     * Identifies the request.  {@link DataUsageRequest}s should only be constructed by
+     * the Framework and it is used internally to identify the request.
+     */
+    public final int requestId;
+
+    /**
+     * {@link NetworkTemplate} describing the network to monitor.
+     */
+    public final NetworkTemplate template;
+
+    /**
+     * Threshold in bytes to be notified on.
+     */
+    public final long thresholdInBytes;
+
+    public DataUsageRequest(int requestId, NetworkTemplate template, long thresholdInBytes) {
+        this.requestId = requestId;
+        this.template = template;
+        this.thresholdInBytes = thresholdInBytes;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(requestId);
+        dest.writeParcelable(template, flags);
+        dest.writeLong(thresholdInBytes);
+    }
+
+    public static final @android.annotation.NonNull Creator<DataUsageRequest> CREATOR =
+            new Creator<DataUsageRequest>() {
+                @Override
+                public DataUsageRequest createFromParcel(Parcel in) {
+                    int requestId = in.readInt();
+                    NetworkTemplate template = in.readParcelable(null);
+                    long thresholdInBytes = in.readLong();
+                    DataUsageRequest result = new DataUsageRequest(requestId, template,
+                            thresholdInBytes);
+                    return result;
+                }
+
+                @Override
+                public DataUsageRequest[] newArray(int size) {
+                    return new DataUsageRequest[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return "DataUsageRequest [ requestId=" + requestId
+                + ", networkTemplate=" + template
+                + ", thresholdInBytes=" + thresholdInBytes + " ]";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof DataUsageRequest == false) return false;
+        DataUsageRequest that = (DataUsageRequest) obj;
+        return that.requestId == this.requestId
+                && Objects.equals(that.template, this.template)
+                && that.thresholdInBytes == this.thresholdInBytes;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(requestId, template, thresholdInBytes);
+   }
+
+}
diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java
new file mode 100644
index 0000000..2b76dd9
--- /dev/null
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.BackgroundThread;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
+
+/**
+ * A class that manages and configures Ethernet interfaces.
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.ETHERNET_SERVICE)
+public class EthernetManager {
+    private static final String TAG = "EthernetManager";
+
+    private final IEthernetManager mService;
+    @GuardedBy("mListenerLock")
+    private final ArrayList<ListenerInfo<InterfaceStateListener>> mIfaceListeners =
+            new ArrayList<>();
+    @GuardedBy("mListenerLock")
+    private final ArrayList<ListenerInfo<IntConsumer>> mEthernetStateListeners =
+            new ArrayList<>();
+    final Object mListenerLock = new Object();
+    private final IEthernetServiceListener.Stub mServiceListener =
+            new IEthernetServiceListener.Stub() {
+                @Override
+                public void onEthernetStateChanged(int state) {
+                    synchronized (mListenerLock) {
+                        for (ListenerInfo<IntConsumer> li : mEthernetStateListeners) {
+                            li.executor.execute(() -> {
+                                li.listener.accept(state);
+                            });
+                        }
+                    }
+                }
+
+                @Override
+                public void onInterfaceStateChanged(String iface, int state, int role,
+                        IpConfiguration configuration) {
+                    synchronized (mListenerLock) {
+                        for (ListenerInfo<InterfaceStateListener> li : mIfaceListeners) {
+                            li.executor.execute(() ->
+                                    li.listener.onInterfaceStateChanged(iface, state, role,
+                                            configuration));
+                        }
+                    }
+                }
+            };
+
+    /**
+     * Indicates that Ethernet is disabled.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int ETHERNET_STATE_DISABLED = 0;
+
+    /**
+     * Indicates that Ethernet is enabled.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int ETHERNET_STATE_ENABLED  = 1;
+
+    private static class ListenerInfo<T> {
+        @NonNull
+        public final Executor executor;
+        @NonNull
+        public final T listener;
+
+        private ListenerInfo(@NonNull Executor executor, @NonNull T listener) {
+            this.executor = executor;
+            this.listener = listener;
+        }
+    }
+
+    /**
+     * The interface is absent.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int STATE_ABSENT = 0;
+
+    /**
+     * The interface is present but link is down.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int STATE_LINK_DOWN = 1;
+
+    /**
+     * The interface is present and link is up.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int STATE_LINK_UP = 2;
+
+    /** @hide */
+    @IntDef(prefix = "STATE_", value = {STATE_ABSENT, STATE_LINK_DOWN, STATE_LINK_UP})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface InterfaceState {}
+
+    /**
+     * The interface currently does not have any specific role.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int ROLE_NONE = 0;
+
+    /**
+     * The interface is in client mode (e.g., connected to the Internet).
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int ROLE_CLIENT = 1;
+
+    /**
+     * Ethernet interface is in server mode (e.g., providing Internet access to tethered devices).
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int ROLE_SERVER = 2;
+
+    /** @hide */
+    @IntDef(prefix = "ROLE_", value = {ROLE_NONE, ROLE_CLIENT, ROLE_SERVER})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Role {}
+
+    /**
+     * A listener that receives notifications about the state of Ethernet interfaces on the system.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public interface InterfaceStateListener {
+        /**
+         * Called when an Ethernet interface changes state.
+         *
+         * @param iface the name of the interface.
+         * @param state the current state of the interface, or {@link #STATE_ABSENT} if the
+         *              interface was removed.
+         * @param role whether the interface is in client mode or server mode.
+         * @param configuration the current IP configuration of the interface.
+         * @hide
+         */
+        @SystemApi(client = MODULE_LIBRARIES)
+        void onInterfaceStateChanged(@NonNull String iface, @InterfaceState int state,
+                @Role int role, @Nullable IpConfiguration configuration);
+    }
+
+    /**
+     * A listener interface to receive notification on changes in Ethernet.
+     * This has never been a supported API. Use {@link InterfaceStateListener} instead.
+     * @hide
+     */
+    public interface Listener extends InterfaceStateListener {
+        /**
+         * Called when Ethernet port's availability is changed.
+         * @param iface Ethernet interface name
+         * @param isAvailable {@code true} if Ethernet port exists.
+         * @hide
+         */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        void onAvailabilityChanged(String iface, boolean isAvailable);
+
+        /** Default implementation for backwards compatibility. Only calls the legacy listener. */
+        default void onInterfaceStateChanged(@NonNull String iface, @InterfaceState int state,
+                @Role int role, @Nullable IpConfiguration configuration) {
+            onAvailabilityChanged(iface, (state >= STATE_LINK_UP));
+        }
+
+    }
+
+    /**
+     * Create a new EthernetManager instance.
+     * Applications will almost always want to use
+     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+     * the standard {@link android.content.Context#ETHERNET_SERVICE Context.ETHERNET_SERVICE}.
+     * @hide
+     */
+    public EthernetManager(Context context, IEthernetManager service) {
+        mService = service;
+    }
+
+    /**
+     * Get Ethernet configuration.
+     * @return the Ethernet Configuration, contained in {@link IpConfiguration}.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public IpConfiguration getConfiguration(String iface) {
+        try {
+            return mService.getConfiguration(iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set Ethernet configuration.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public void setConfiguration(@NonNull String iface, @NonNull IpConfiguration config) {
+        try {
+            mService.setConfiguration(iface, config);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Indicates whether the system currently has one or more Ethernet interfaces.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public boolean isAvailable() {
+        return getAvailableInterfaces().length > 0;
+    }
+
+    /**
+     * Indicates whether the system has given interface.
+     *
+     * @param iface Ethernet interface name
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public boolean isAvailable(String iface) {
+        try {
+            return mService.isAvailable(iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Adds a listener.
+     * This has never been a supported API. Use {@link #addInterfaceStateListener} instead.
+     *
+     * @param listener A {@link Listener} to add.
+     * @throws IllegalArgumentException If the listener is null.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public void addListener(@NonNull Listener listener) {
+        addListener(listener, BackgroundThread.getExecutor());
+    }
+
+    /**
+     * Adds a listener.
+     * This has never been a supported API. Use {@link #addInterfaceStateListener} instead.
+     *
+     * @param listener A {@link Listener} to add.
+     * @param executor Executor to run callbacks on.
+     * @throws IllegalArgumentException If the listener or executor is null.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public void addListener(@NonNull Listener listener, @NonNull Executor executor) {
+        addInterfaceStateListener(executor, listener);
+    }
+
+    /**
+     * Listen to changes in the state of Ethernet interfaces.
+     *
+     * Adds a listener to receive notification for any state change of all existing Ethernet
+     * interfaces.
+     * <p>{@link Listener#onInterfaceStateChanged} will be triggered immediately for all
+     * existing interfaces upon adding a listener. The same method will be called on the
+     * listener every time any of the interface changes state. In particular, if an
+     * interface is removed, it will be called with state {@link #STATE_ABSENT}.
+     * <p>Use {@link #removeInterfaceStateListener} with the same object to stop listening.
+     *
+     * @param executor Executor to run callbacks on.
+     * @param listener A {@link Listener} to add.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void addInterfaceStateListener(@NonNull Executor executor,
+            @NonNull InterfaceStateListener listener) {
+        if (listener == null || executor == null) {
+            throw new NullPointerException("listener and executor must not be null");
+        }
+        synchronized (mListenerLock) {
+            maybeAddServiceListener();
+            mIfaceListeners.add(new ListenerInfo<InterfaceStateListener>(executor, listener));
+        }
+    }
+
+    @GuardedBy("mListenerLock")
+    private void maybeAddServiceListener() {
+        if (!mIfaceListeners.isEmpty() || !mEthernetStateListeners.isEmpty()) return;
+
+        try {
+            mService.addListener(mServiceListener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+    }
+
+    /**
+     * Returns an array of available Ethernet interface names.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public String[] getAvailableInterfaces() {
+        try {
+            return mService.getAvailableInterfaces();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Removes a listener.
+     *
+     * @param listener A {@link Listener} to remove.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void removeInterfaceStateListener(@NonNull InterfaceStateListener listener) {
+        Objects.requireNonNull(listener);
+        synchronized (mListenerLock) {
+            mIfaceListeners.removeIf(l -> l.listener == listener);
+            maybeRemoveServiceListener();
+        }
+    }
+
+    @GuardedBy("mListenerLock")
+    private void maybeRemoveServiceListener() {
+        if (!mIfaceListeners.isEmpty() || !mEthernetStateListeners.isEmpty()) return;
+
+        try {
+            mService.removeListener(mServiceListener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Removes a listener.
+     * This has never been a supported API. Use {@link #removeInterfaceStateListener} instead.
+     * @param listener A {@link Listener} to remove.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public void removeListener(@NonNull Listener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+        removeInterfaceStateListener(listener);
+    }
+
+    /**
+     * Whether to treat interfaces created by {@link TestNetworkManager#createTapInterface}
+     * as Ethernet interfaces. The effects of this method apply to any test interfaces that are
+     * already present on the system.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void setIncludeTestInterfaces(boolean include) {
+        try {
+            mService.setIncludeTestInterfaces(include);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * A request for a tethered interface.
+     */
+    public static class TetheredInterfaceRequest {
+        private final IEthernetManager mService;
+        private final ITetheredInterfaceCallback mCb;
+
+        private TetheredInterfaceRequest(@NonNull IEthernetManager service,
+                @NonNull ITetheredInterfaceCallback cb) {
+            this.mService = service;
+            this.mCb = cb;
+        }
+
+        /**
+         * Release the request, causing the interface to revert back from tethering mode if there
+         * is no other requestor.
+         */
+        public void release() {
+            try {
+                mService.releaseTetheredInterface(mCb);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Callback for {@link #requestTetheredInterface(TetheredInterfaceCallback)}.
+     */
+    public interface TetheredInterfaceCallback {
+        /**
+         * Called when the tethered interface is available.
+         * @param iface The name of the interface.
+         */
+        void onAvailable(@NonNull String iface);
+
+        /**
+         * Called when the tethered interface is now unavailable.
+         */
+        void onUnavailable();
+    }
+
+    /**
+     * Request a tethered interface in tethering mode.
+     *
+     * <p>When this method is called and there is at least one ethernet interface available, the
+     * system will designate one to act as a tethered interface. If there is already a tethered
+     * interface, the existing interface will be used.
+     * @param callback A callback to be called once the request has been fulfilled.
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_STACK,
+            android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    @NonNull
+    public TetheredInterfaceRequest requestTetheredInterface(@NonNull final Executor executor,
+            @NonNull final TetheredInterfaceCallback callback) {
+        Objects.requireNonNull(callback, "Callback must be non-null");
+        Objects.requireNonNull(executor, "Executor must be non-null");
+        final ITetheredInterfaceCallback cbInternal = new ITetheredInterfaceCallback.Stub() {
+            @Override
+            public void onAvailable(String iface) {
+                executor.execute(() -> callback.onAvailable(iface));
+            }
+
+            @Override
+            public void onUnavailable() {
+                executor.execute(() -> callback.onUnavailable());
+            }
+        };
+
+        try {
+            mService.requestTetheredInterface(cbInternal);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return new TetheredInterfaceRequest(mService, cbInternal);
+    }
+
+    private static final class NetworkInterfaceOutcomeReceiver
+            extends INetworkInterfaceOutcomeReceiver.Stub {
+        @NonNull
+        private final Executor mExecutor;
+        @NonNull
+        private final OutcomeReceiver<String, EthernetNetworkManagementException> mCallback;
+
+        NetworkInterfaceOutcomeReceiver(
+                @NonNull final Executor executor,
+                @NonNull final OutcomeReceiver<String, EthernetNetworkManagementException>
+                        callback) {
+            Objects.requireNonNull(executor, "Pass a non-null executor");
+            Objects.requireNonNull(callback, "Pass a non-null callback");
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onResult(@NonNull String iface) {
+            mExecutor.execute(() -> mCallback.onResult(iface));
+        }
+
+        @Override
+        public void onError(@NonNull EthernetNetworkManagementException e) {
+            mExecutor.execute(() -> mCallback.onError(e));
+        }
+    }
+
+    private NetworkInterfaceOutcomeReceiver makeNetworkInterfaceOutcomeReceiver(
+            @Nullable final Executor executor,
+            @Nullable final OutcomeReceiver<String, EthernetNetworkManagementException> callback) {
+        if (null != callback) {
+            Objects.requireNonNull(executor, "Pass a non-null executor, or a null callback");
+        }
+        final NetworkInterfaceOutcomeReceiver proxy;
+        if (null == callback) {
+            proxy = null;
+        } else {
+            proxy = new NetworkInterfaceOutcomeReceiver(executor, callback);
+        }
+        return proxy;
+    }
+
+    /**
+     * Updates the configuration of an automotive device's ethernet network.
+     *
+     * The {@link EthernetNetworkUpdateRequest} {@code request} argument describes how to update the
+     * configuration for this network.
+     * Use {@link StaticIpConfiguration.Builder} to build a {@code StaticIpConfiguration} object for
+     * this network to put inside the {@code request}.
+     * Similarly, use {@link NetworkCapabilities.Builder} to build a {@code NetworkCapabilities}
+     * object for this network to put inside the {@code request}.
+     *
+     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
+     *
+     * @param iface the name of the interface to act upon.
+     * @param request the {@link EthernetNetworkUpdateRequest} used to set an ethernet network's
+     *                {@link StaticIpConfiguration} and {@link NetworkCapabilities} values.
+     * @param executor an {@link Executor} to execute the callback on. Optional if callback is null.
+     * @param callback an optional {@link OutcomeReceiver} to listen for completion of the
+     *                 operation. On success, {@link OutcomeReceiver#onResult} is called with the
+     *                 interface name. On error, {@link OutcomeReceiver#onError} is called with more
+     *                 information about the error.
+     * @throws SecurityException if the process doesn't hold
+     *                          {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}.
+     * @throws UnsupportedOperationException if the {@link NetworkCapabilities} are updated on a
+     *                                       non-automotive device or this function is called on an
+     *                                       unsupported interface.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
+    public void updateConfiguration(
+            @NonNull String iface,
+            @NonNull EthernetNetworkUpdateRequest request,
+            @Nullable @CallbackExecutor Executor executor,
+            @Nullable OutcomeReceiver<String, EthernetNetworkManagementException> callback) {
+        Objects.requireNonNull(iface, "iface must be non-null");
+        Objects.requireNonNull(request, "request must be non-null");
+        final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
+                executor, callback);
+        try {
+            mService.updateConfiguration(iface, request, proxy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Enable a network interface.
+     *
+     * Enables a previously disabled network interface. An attempt to enable an already-enabled
+     * interface is ignored.
+     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
+     *
+     * @param iface the name of the interface to enable.
+     * @param executor an {@link Executor} to execute the callback on. Optional if callback is null.
+     * @param callback an optional {@link OutcomeReceiver} to listen for completion of the
+     *                 operation. On success, {@link OutcomeReceiver#onResult} is called with the
+     *                 interface name. On error, {@link OutcomeReceiver#onError} is called with more
+     *                 information about the error.
+     * @throws SecurityException if the process doesn't hold
+     *                          {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
+    @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
+    public void enableInterface(
+            @NonNull String iface,
+            @Nullable @CallbackExecutor Executor executor,
+            @Nullable OutcomeReceiver<String, EthernetNetworkManagementException> callback) {
+        Objects.requireNonNull(iface, "iface must be non-null");
+        final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
+                executor, callback);
+        try {
+            mService.connectNetwork(iface, proxy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Disable a network interface.
+     *
+     * Disables the specified interface. If this interface is in use in a connected
+     * {@link android.net.Network}, then that {@code Network} will be torn down.
+     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
+     *
+     * @param iface the name of the interface to disable.
+     * @param executor an {@link Executor} to execute the callback on. Optional if callback is null.
+     * @param callback an optional {@link OutcomeReceiver} to listen for completion of the
+     *                 operation. On success, {@link OutcomeReceiver#onResult} is called with the
+     *                 interface name. On error, {@link OutcomeReceiver#onError} is called with more
+     *                 information about the error.
+     * @throws SecurityException if the process doesn't hold
+     *                          {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
+    @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
+    public void disableInterface(
+            @NonNull String iface,
+            @Nullable @CallbackExecutor Executor executor,
+            @Nullable OutcomeReceiver<String, EthernetNetworkManagementException> callback) {
+        Objects.requireNonNull(iface, "iface must be non-null");
+        final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
+                executor, callback);
+        try {
+            mService.disconnectNetwork(iface, proxy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Change ethernet setting.
+     *
+     * @param enabled enable or disable ethernet settings.
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void setEthernetEnabled(boolean enabled) {
+        try {
+            mService.setEthernetEnabled(enabled);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Listen to changes in the state of ethernet.
+     *
+     * @param executor to run callbacks on.
+     * @param listener to listen ethernet state changed.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void addEthernetStateListener(@NonNull Executor executor,
+            @NonNull IntConsumer listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mListenerLock) {
+            maybeAddServiceListener();
+            mEthernetStateListeners.add(new ListenerInfo<IntConsumer>(executor, listener));
+        }
+    }
+
+    /**
+     * Removes a listener.
+     *
+     * @param listener to listen ethernet state changed.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void removeEthernetStateListener(@NonNull IntConsumer listener) {
+        Objects.requireNonNull(listener);
+        synchronized (mListenerLock) {
+            mEthernetStateListeners.removeIf(l -> l.listener == listener);
+            maybeRemoveServiceListener();
+        }
+    }
+
+    /**
+     * Returns an array of existing Ethernet interface names regardless whether the interface
+     * is available or not currently.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @SystemApi(client = MODULE_LIBRARIES)
+    @NonNull
+    public List<String> getInterfaceList() {
+        try {
+            return mService.getInterfaceList();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+}
diff --git a/framework-t/src/android/net/EthernetNetworkManagementException.aidl b/framework-t/src/android/net/EthernetNetworkManagementException.aidl
new file mode 100644
index 0000000..adf9e5a
--- /dev/null
+++ b/framework-t/src/android/net/EthernetNetworkManagementException.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package android.net;
+
+ parcelable EthernetNetworkManagementException;
\ No newline at end of file
diff --git a/framework-t/src/android/net/EthernetNetworkManagementException.java b/framework-t/src/android/net/EthernetNetworkManagementException.java
new file mode 100644
index 0000000..a69cc55
--- /dev/null
+++ b/framework-t/src/android/net/EthernetNetworkManagementException.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/** @hide */
+@SystemApi
+public final class EthernetNetworkManagementException
+        extends RuntimeException implements Parcelable {
+
+    /* @hide */
+    public EthernetNetworkManagementException(@NonNull final String errorMessage) {
+        super(errorMessage);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getMessage());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null || getClass() != obj.getClass()) return false;
+        final EthernetNetworkManagementException that = (EthernetNetworkManagementException) obj;
+
+        return Objects.equals(getMessage(), that.getMessage());
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(getMessage());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<EthernetNetworkManagementException> CREATOR =
+            new Parcelable.Creator<EthernetNetworkManagementException>() {
+                @Override
+                public EthernetNetworkManagementException[] newArray(int size) {
+                    return new EthernetNetworkManagementException[size];
+                }
+
+                @Override
+                public EthernetNetworkManagementException createFromParcel(@NonNull Parcel source) {
+                    return new EthernetNetworkManagementException(source.readString());
+                }
+            };
+}
diff --git a/framework-t/src/android/net/EthernetNetworkSpecifier.java b/framework-t/src/android/net/EthernetNetworkSpecifier.java
new file mode 100644
index 0000000..e4d6e24
--- /dev/null
+++ b/framework-t/src/android/net/EthernetNetworkSpecifier.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * A {@link NetworkSpecifier} used to identify ethernet interfaces.
+ *
+ * @see EthernetManager
+ */
+public final class EthernetNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+
+    /**
+     * Name of the network interface.
+     */
+    @NonNull
+    private final String mInterfaceName;
+
+    /**
+     * Create a new EthernetNetworkSpecifier.
+     * @param interfaceName Name of the ethernet interface the specifier refers to.
+     */
+    public EthernetNetworkSpecifier(@NonNull String interfaceName) {
+        if (TextUtils.isEmpty(interfaceName)) {
+            throw new IllegalArgumentException();
+        }
+        mInterfaceName = interfaceName;
+    }
+
+    /**
+     * Get the name of the ethernet interface the specifier refers to.
+     */
+    @Nullable
+    public String getInterfaceName() {
+        // This may be null in the future to support specifiers based on data other than the
+        // interface name.
+        return mInterfaceName;
+    }
+
+    /** @hide */
+    @Override
+    public boolean canBeSatisfiedBy(@Nullable NetworkSpecifier other) {
+        return equals(other);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (!(o instanceof EthernetNetworkSpecifier)) return false;
+        return TextUtils.equals(mInterfaceName, ((EthernetNetworkSpecifier) o).mInterfaceName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mInterfaceName);
+    }
+
+    @Override
+    public String toString() {
+        return "EthernetNetworkSpecifier (" + mInterfaceName + ")";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(mInterfaceName);
+    }
+
+    public static final @NonNull Parcelable.Creator<EthernetNetworkSpecifier> CREATOR =
+            new Parcelable.Creator<EthernetNetworkSpecifier>() {
+        public EthernetNetworkSpecifier createFromParcel(Parcel in) {
+            return new EthernetNetworkSpecifier(in.readString());
+        }
+        public EthernetNetworkSpecifier[] newArray(int size) {
+            return new EthernetNetworkSpecifier[size];
+        }
+    };
+}
diff --git a/framework-t/src/android/net/EthernetNetworkUpdateRequest.aidl b/framework-t/src/android/net/EthernetNetworkUpdateRequest.aidl
new file mode 100644
index 0000000..debc348
--- /dev/null
+++ b/framework-t/src/android/net/EthernetNetworkUpdateRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package android.net;
+
+ parcelable EthernetNetworkUpdateRequest;
\ No newline at end of file
diff --git a/framework-t/src/android/net/EthernetNetworkUpdateRequest.java b/framework-t/src/android/net/EthernetNetworkUpdateRequest.java
new file mode 100644
index 0000000..1691942
--- /dev/null
+++ b/framework-t/src/android/net/EthernetNetworkUpdateRequest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represents a request to update an existing Ethernet interface.
+ *
+ * @see EthernetManager#updateConfiguration
+ *
+ * @hide
+ */
+@SystemApi
+public final class EthernetNetworkUpdateRequest implements Parcelable {
+    @Nullable
+    private final IpConfiguration mIpConfig;
+    @Nullable
+    private final NetworkCapabilities mNetworkCapabilities;
+
+    /**
+     * Setting the {@link IpConfiguration} is optional in {@link EthernetNetworkUpdateRequest}.
+     * When set to null, the existing IpConfiguration is not updated.
+     *
+     * @return the new {@link IpConfiguration} or null.
+     */
+    @Nullable
+    public IpConfiguration getIpConfiguration() {
+        return mIpConfig == null ? null : new IpConfiguration(mIpConfig);
+    }
+
+    /**
+     * Setting the {@link NetworkCapabilities} is optional in {@link EthernetNetworkUpdateRequest}.
+     * When set to null, the existing NetworkCapabilities are not updated.
+     *
+     * @return the new {@link NetworkCapabilities} or null.
+     */
+    @Nullable
+    public NetworkCapabilities getNetworkCapabilities() {
+        return mNetworkCapabilities == null ? null : new NetworkCapabilities(mNetworkCapabilities);
+    }
+
+    private EthernetNetworkUpdateRequest(@Nullable final IpConfiguration ipConfig,
+            @Nullable final NetworkCapabilities networkCapabilities) {
+        mIpConfig = ipConfig;
+        mNetworkCapabilities = networkCapabilities;
+    }
+
+    private EthernetNetworkUpdateRequest(@NonNull final Parcel source) {
+        Objects.requireNonNull(source);
+        mIpConfig = source.readParcelable(IpConfiguration.class.getClassLoader(),
+                IpConfiguration.class);
+        mNetworkCapabilities = source.readParcelable(NetworkCapabilities.class.getClassLoader(),
+                NetworkCapabilities.class);
+    }
+
+    /**
+     * Builder used to create {@link EthernetNetworkUpdateRequest} objects.
+     */
+    public static final class Builder {
+        @Nullable
+        private IpConfiguration mBuilderIpConfig;
+        @Nullable
+        private NetworkCapabilities mBuilderNetworkCapabilities;
+
+        public Builder(){}
+
+        /**
+         * Constructor to populate the builder's values with an already built
+         * {@link EthernetNetworkUpdateRequest}.
+         * @param request the {@link EthernetNetworkUpdateRequest} to populate with.
+         */
+        public Builder(@NonNull final EthernetNetworkUpdateRequest request) {
+            Objects.requireNonNull(request);
+            mBuilderIpConfig = null == request.mIpConfig
+                    ? null : new IpConfiguration(request.mIpConfig);
+            mBuilderNetworkCapabilities = null == request.mNetworkCapabilities
+                    ? null : new NetworkCapabilities(request.mNetworkCapabilities);
+        }
+
+        /**
+         * Set the {@link IpConfiguration} to be used with the {@code Builder}.
+         * @param ipConfig the {@link IpConfiguration} to set.
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder setIpConfiguration(@Nullable final IpConfiguration ipConfig) {
+            mBuilderIpConfig = ipConfig == null ? null : new IpConfiguration(ipConfig);
+            return this;
+        }
+
+        /**
+         * Set the {@link NetworkCapabilities} to be used with the {@code Builder}.
+         * @param nc the {@link NetworkCapabilities} to set.
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder setNetworkCapabilities(@Nullable final NetworkCapabilities nc) {
+            mBuilderNetworkCapabilities = nc == null ? null : new NetworkCapabilities(nc);
+            return this;
+        }
+
+        /**
+         * Build {@link EthernetNetworkUpdateRequest} return the current update request.
+         *
+         * @throws IllegalStateException when both mBuilderNetworkCapabilities and mBuilderIpConfig
+         *                               are null.
+         */
+        @NonNull
+        public EthernetNetworkUpdateRequest build() {
+            if (mBuilderIpConfig == null && mBuilderNetworkCapabilities == null) {
+                throw new IllegalStateException(
+                        "Cannot construct an empty EthernetNetworkUpdateRequest");
+            }
+            return new EthernetNetworkUpdateRequest(mBuilderIpConfig, mBuilderNetworkCapabilities);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "EthernetNetworkUpdateRequest{"
+                + "mIpConfig=" + mIpConfig
+                + ", mNetworkCapabilities=" + mNetworkCapabilities + '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        EthernetNetworkUpdateRequest that = (EthernetNetworkUpdateRequest) o;
+
+        return Objects.equals(that.getIpConfiguration(), mIpConfig)
+                && Objects.equals(that.getNetworkCapabilities(), mNetworkCapabilities);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIpConfig, mNetworkCapabilities);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mIpConfig, flags);
+        dest.writeParcelable(mNetworkCapabilities, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<EthernetNetworkUpdateRequest> CREATOR =
+            new Parcelable.Creator<EthernetNetworkUpdateRequest>() {
+                @Override
+                public EthernetNetworkUpdateRequest[] newArray(int size) {
+                    return new EthernetNetworkUpdateRequest[size];
+                }
+
+                @Override
+                public EthernetNetworkUpdateRequest createFromParcel(@NonNull Parcel source) {
+                    return new EthernetNetworkUpdateRequest(source);
+                }
+            };
+}
diff --git a/framework-t/src/android/net/IEthernetManager.aidl b/framework-t/src/android/net/IEthernetManager.aidl
new file mode 100644
index 0000000..42e4c1a
--- /dev/null
+++ b/framework-t/src/android/net/IEthernetManager.aidl
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.IpConfiguration;
+import android.net.IEthernetServiceListener;
+import android.net.EthernetNetworkManagementException;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.ITetheredInterfaceCallback;
+
+import java.util.List;
+
+/**
+ * Interface that answers queries about, and allows changing
+ * ethernet configuration.
+ */
+/** {@hide} */
+interface IEthernetManager
+{
+    String[] getAvailableInterfaces();
+    IpConfiguration getConfiguration(String iface);
+    void setConfiguration(String iface, in IpConfiguration config);
+    boolean isAvailable(String iface);
+    void addListener(in IEthernetServiceListener listener);
+    void removeListener(in IEthernetServiceListener listener);
+    void setIncludeTestInterfaces(boolean include);
+    void requestTetheredInterface(in ITetheredInterfaceCallback callback);
+    void releaseTetheredInterface(in ITetheredInterfaceCallback callback);
+    void updateConfiguration(String iface, in EthernetNetworkUpdateRequest request,
+        in INetworkInterfaceOutcomeReceiver listener);
+    void connectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener);
+    void disconnectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener);
+    void setEthernetEnabled(boolean enabled);
+    List<String> getInterfaceList();
+}
diff --git a/framework-t/src/android/net/IEthernetServiceListener.aidl b/framework-t/src/android/net/IEthernetServiceListener.aidl
new file mode 100644
index 0000000..751605b
--- /dev/null
+++ b/framework-t/src/android/net/IEthernetServiceListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.IpConfiguration;
+
+/** @hide */
+oneway interface IEthernetServiceListener
+{
+    void onEthernetStateChanged(int state);
+    void onInterfaceStateChanged(String iface, int state, int role,
+            in IpConfiguration configuration);
+}
diff --git a/framework-t/src/android/net/IIpSecService.aidl b/framework-t/src/android/net/IIpSecService.aidl
new file mode 100644
index 0000000..933256a
--- /dev/null
+++ b/framework-t/src/android/net/IIpSecService.aidl
@@ -0,0 +1,78 @@
+/*
+** Copyright 2017, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.net;
+
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.IpSecConfig;
+import android.net.IpSecUdpEncapResponse;
+import android.net.IpSecSpiResponse;
+import android.net.IpSecTransformResponse;
+import android.net.IpSecTunnelInterfaceResponse;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * @hide
+ */
+interface IIpSecService
+{
+    IpSecSpiResponse allocateSecurityParameterIndex(
+            in String destinationAddress, int requestedSpi, in IBinder binder);
+
+    void releaseSecurityParameterIndex(int resourceId);
+
+    IpSecUdpEncapResponse openUdpEncapsulationSocket(int port, in IBinder binder);
+
+    void closeUdpEncapsulationSocket(int resourceId);
+
+    IpSecTunnelInterfaceResponse createTunnelInterface(
+            in String localAddr,
+            in String remoteAddr,
+            in Network underlyingNetwork,
+            in IBinder binder,
+            in String callingPackage);
+
+    void addAddressToTunnelInterface(
+            int tunnelResourceId,
+            in LinkAddress localAddr,
+            in String callingPackage);
+
+    void removeAddressFromTunnelInterface(
+            int tunnelResourceId,
+            in LinkAddress localAddr,
+            in String callingPackage);
+
+    void setNetworkForTunnelInterface(
+            int tunnelResourceId, in Network underlyingNetwork, in String callingPackage);
+
+    void deleteTunnelInterface(int resourceId, in String callingPackage);
+
+    IpSecTransformResponse createTransform(
+            in IpSecConfig c, in IBinder binder, in String callingPackage);
+
+    void deleteTransform(int transformId);
+
+    void applyTransportModeTransform(
+            in ParcelFileDescriptor socket, int direction, int transformId);
+
+    void applyTunnelModeTransform(
+            int tunnelResourceId, int direction, int transformResourceId, in String callingPackage);
+
+    void removeTransportModeTransforms(in ParcelFileDescriptor socket);
+}
diff --git a/framework-t/src/android/net/INetworkInterfaceOutcomeReceiver.aidl b/framework-t/src/android/net/INetworkInterfaceOutcomeReceiver.aidl
new file mode 100644
index 0000000..85795ea
--- /dev/null
+++ b/framework-t/src/android/net/INetworkInterfaceOutcomeReceiver.aidl
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.EthernetNetworkManagementException;
+
+/** @hide */
+oneway interface INetworkInterfaceOutcomeReceiver {
+    void onResult(in String iface);
+    void onError(in EthernetNetworkManagementException e);
+}
\ No newline at end of file
diff --git a/framework-t/src/android/net/INetworkStatsService.aidl b/framework-t/src/android/net/INetworkStatsService.aidl
new file mode 100644
index 0000000..c86f7fd
--- /dev/null
+++ b/framework-t/src/android/net/INetworkStatsService.aidl
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.DataUsageRequest;
+import android.net.INetworkStatsSession;
+import android.net.Network;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.UnderlyingNetworkInfo;
+import android.net.netstats.IUsageCallback;
+import android.net.netstats.provider.INetworkStatsProvider;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.os.IBinder;
+import android.os.Messenger;
+
+/** {@hide} */
+interface INetworkStatsService {
+
+    /** Start a statistics query session. */
+    @UnsupportedAppUsage
+    INetworkStatsSession openSession();
+
+    /** Start a statistics query session. If calling package is profile or device owner then it is
+     *  granted automatic access if apiLevel is NetworkStatsManager.API_LEVEL_DPC_ALLOWED. If
+     *  apiLevel is at least NetworkStatsManager.API_LEVEL_REQUIRES_PACKAGE_USAGE_STATS then
+     *  PACKAGE_USAGE_STATS permission is always checked. If PACKAGE_USAGE_STATS is not granted
+     *  READ_NETWORK_USAGE_STATS is checked for.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
+    INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage);
+
+    /** Return data layer snapshot of UID network usage. */
+    @UnsupportedAppUsage
+    NetworkStats getDataLayerSnapshotForUid(int uid);
+
+    /** Get the transport NetworkStats for all UIDs since boot. */
+    NetworkStats getUidStatsForTransport(int transport);
+
+    /** Return set of any ifaces associated with mobile networks since boot. */
+    @UnsupportedAppUsage
+    String[] getMobileIfaces();
+
+    /** Increment data layer count of operations performed for UID and tag. */
+    void incrementOperationCount(int uid, int tag, int operationCount);
+
+    /**  Notify {@code NetworkStatsService} about network status changed. */
+    void notifyNetworkStatus(
+         in Network[] defaultNetworks,
+         in NetworkStateSnapshot[] snapshots,
+         in String activeIface,
+         in UnderlyingNetworkInfo[] underlyingNetworkInfos);
+    /** Force update of statistics. */
+    @UnsupportedAppUsage
+    void forceUpdate();
+
+    /** Registers a callback on data usage. */
+    DataUsageRequest registerUsageCallback(String callingPackage,
+            in DataUsageRequest request, in IUsageCallback callback);
+
+    /** Unregisters a callback on data usage. */
+    void unregisterUsageRequest(in DataUsageRequest request);
+
+    /** Get the uid stats information since boot */
+    long getUidStats(int uid, int type);
+
+    /** Get the iface stats information since boot */
+    long getIfaceStats(String iface, int type);
+
+    /** Get the total network stats information since boot */
+    long getTotalStats(int type);
+
+    /** Registers a network stats provider */
+    INetworkStatsProviderCallback registerNetworkStatsProvider(String tag,
+            in INetworkStatsProvider provider);
+
+    /** Mark given UID as being in foreground for stats purposes. */
+    void noteUidForeground(int uid, boolean uidForeground);
+
+    /** Advise persistence threshold; may be overridden internally. */
+    void advisePersistThreshold(long thresholdBytes);
+
+    /**
+     * Set the warning and limit to all registered custom network stats providers.
+     * Note that invocation of any interface will be sent to all providers.
+     */
+     void setStatsProviderWarningAndLimitAsync(String iface, long warning, long limit);
+}
diff --git a/framework-t/src/android/net/INetworkStatsSession.aidl b/framework-t/src/android/net/INetworkStatsSession.aidl
new file mode 100644
index 0000000..ab70be8
--- /dev/null
+++ b/framework-t/src/android/net/INetworkStatsSession.aidl
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+
+/** {@hide} */
+interface INetworkStatsSession {
+
+    /** Return device aggregated network layer usage summary for traffic that matches template. */
+    NetworkStats getDeviceSummaryForNetwork(in NetworkTemplate template, long start, long end);
+
+    /** Return network layer usage summary for traffic that matches template. */
+    @UnsupportedAppUsage
+    NetworkStats getSummaryForNetwork(in NetworkTemplate template, long start, long end);
+    /** Return historical network layer stats for traffic that matches template. */
+    @UnsupportedAppUsage
+    NetworkStatsHistory getHistoryForNetwork(in NetworkTemplate template, int fields);
+    /**
+     * Return historical network layer stats for traffic that matches template, start and end
+     * timestamp.
+     */
+    NetworkStatsHistory getHistoryIntervalForNetwork(in NetworkTemplate template, int fields, long start, long end);
+
+    /**
+     * Return network layer usage summary per UID for traffic that matches template.
+     *
+     * <p>The resulting {@code NetworkStats#getElapsedRealtime()} contains time delta between
+     * {@code start} and {@code end}.
+     *
+     * @param template - a predicate to filter netstats.
+     * @param start - start of the range, timestamp in milliseconds since the epoch.
+     * @param end - end of the range, timestamp in milliseconds since the epoch.
+     * @param includeTags - includes data usage tags if true.
+     */
+    @UnsupportedAppUsage
+    NetworkStats getSummaryForAllUid(in NetworkTemplate template, long start, long end, boolean includeTags);
+
+    /** Return network layer usage summary per UID for tagged traffic that matches template. */
+    NetworkStats getTaggedSummaryForAllUid(in NetworkTemplate template, long start, long end);
+
+    /** Return historical network layer stats for specific UID traffic that matches template. */
+    @UnsupportedAppUsage
+    NetworkStatsHistory getHistoryForUid(in NetworkTemplate template, int uid, int set, int tag, int fields);
+    /** Return historical network layer stats for specific UID traffic that matches template. */
+    NetworkStatsHistory getHistoryIntervalForUid(in NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end);
+
+    /** Return array of uids that have stats and are accessible to the calling user */
+    int[] getRelevantUids();
+
+    @UnsupportedAppUsage
+    void close();
+
+}
diff --git a/framework-t/src/android/net/ITetheredInterfaceCallback.aidl b/framework-t/src/android/net/ITetheredInterfaceCallback.aidl
new file mode 100644
index 0000000..14aa023
--- /dev/null
+++ b/framework-t/src/android/net/ITetheredInterfaceCallback.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/** @hide */
+oneway interface ITetheredInterfaceCallback {
+    void onAvailable(in String iface);
+    void onUnavailable();
+}
\ No newline at end of file
diff --git a/framework-t/src/android/net/IpSecAlgorithm.java b/framework-t/src/android/net/IpSecAlgorithm.java
new file mode 100644
index 0000000..10a22ac
--- /dev/null
+++ b/framework-t/src/android/net/IpSecAlgorithm.java
@@ -0,0 +1,491 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.StringDef;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * This class represents a single algorithm that can be used by an {@link IpSecTransform}.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
+ * Internet Protocol</a>
+ */
+public final class IpSecAlgorithm implements Parcelable {
+    private static final String TAG = "IpSecAlgorithm";
+
+    /**
+     * Null cipher.
+     *
+     * @hide
+     */
+    public static final String CRYPT_NULL = "ecb(cipher_null)";
+
+    /**
+     * AES-CBC Encryption/Ciphering Algorithm.
+     *
+     * <p>Valid lengths for this key are {128, 192, 256}.
+     */
+    public static final String CRYPT_AES_CBC = "cbc(aes)";
+
+    /**
+     * AES-CTR Encryption/Ciphering Algorithm.
+     *
+     * <p>Valid lengths for keying material are {160, 224, 288}.
+     *
+     * <p>As per <a href="https://tools.ietf.org/html/rfc3686#section-5.1">RFC3686 (Section
+     * 5.1)</a>, keying material consists of a 128, 192, or 256 bit AES key followed by a 32-bit
+     * nonce. RFC compliance requires that the nonce must be unique per security association.
+     *
+     * <p>This algorithm may be available on the device. Caller MUST check if it is supported before
+     * using it by calling {@link #getSupportedAlgorithms()} and checking if this algorithm is
+     * included in the returned algorithm set. The returned algorithm set will not change unless the
+     * device is rebooted. {@link IllegalArgumentException} will be thrown if this algorithm is
+     * requested on an unsupported device.
+     *
+     * <p>@see {@link #getSupportedAlgorithms()}
+     */
+    // This algorithm may be available on devices released before Android 12, and is guaranteed
+    // to be available on devices first shipped with Android 12 or later.
+    public static final String CRYPT_AES_CTR = "rfc3686(ctr(aes))";
+
+    /**
+     * MD5 HMAC Authentication/Integrity Algorithm. <b>This algorithm is not recommended for use in
+     * new applications and is provided for legacy compatibility with 3gpp infrastructure.</b>
+     *
+     * <p>Keys for this algorithm must be 128 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 96 to 128.
+     */
+    public static final String AUTH_HMAC_MD5 = "hmac(md5)";
+
+    /**
+     * SHA1 HMAC Authentication/Integrity Algorithm. <b>This algorithm is not recommended for use in
+     * new applications and is provided for legacy compatibility with 3gpp infrastructure.</b>
+     *
+     * <p>Keys for this algorithm must be 160 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 96 to 160.
+     */
+    public static final String AUTH_HMAC_SHA1 = "hmac(sha1)";
+
+    /**
+     * SHA256 HMAC Authentication/Integrity Algorithm.
+     *
+     * <p>Keys for this algorithm must be 256 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 96 to 256.
+     */
+    public static final String AUTH_HMAC_SHA256 = "hmac(sha256)";
+
+    /**
+     * SHA384 HMAC Authentication/Integrity Algorithm.
+     *
+     * <p>Keys for this algorithm must be 384 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 192 to 384.
+     */
+    public static final String AUTH_HMAC_SHA384 = "hmac(sha384)";
+
+    /**
+     * SHA512 HMAC Authentication/Integrity Algorithm.
+     *
+     * <p>Keys for this algorithm must be 512 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 256 to 512.
+     */
+    public static final String AUTH_HMAC_SHA512 = "hmac(sha512)";
+
+    /**
+     * AES-XCBC Authentication/Integrity Algorithm.
+     *
+     * <p>Keys for this algorithm must be 128 bits in length.
+     *
+     * <p>The only valid truncation length is 96 bits.
+     *
+     * <p>This algorithm may be available on the device. Caller MUST check if it is supported before
+     * using it by calling {@link #getSupportedAlgorithms()} and checking if this algorithm is
+     * included in the returned algorithm set. The returned algorithm set will not change unless the
+     * device is rebooted. {@link IllegalArgumentException} will be thrown if this algorithm is
+     * requested on an unsupported device.
+     *
+     * <p>@see {@link #getSupportedAlgorithms()}
+     */
+    // This algorithm may be available on devices released before Android 12, and is guaranteed
+    // to be available on devices first shipped with Android 12 or later.
+    public static final String AUTH_AES_XCBC = "xcbc(aes)";
+
+    /**
+     * AES-CMAC Authentication/Integrity Algorithm.
+     *
+     * <p>Keys for this algorithm must be 128 bits in length.
+     *
+     * <p>The only valid truncation length is 96 bits.
+     *
+     * <p>This algorithm may be available on the device. Caller MUST check if it is supported before
+     * using it by calling {@link #getSupportedAlgorithms()} and checking if this algorithm is
+     * included in the returned algorithm set. The returned algorithm set will not change unless the
+     * device is rebooted. {@link IllegalArgumentException} will be thrown if this algorithm is
+     * requested on an unsupported device.
+     *
+     * <p>@see {@link #getSupportedAlgorithms()}
+     */
+    // This algorithm may be available on devices released before Android 12, and is guaranteed
+    // to be available on devices first shipped with Android 12 or later.
+    public static final String AUTH_AES_CMAC = "cmac(aes)";
+
+    /**
+     * AES-GCM Authentication/Integrity + Encryption/Ciphering Algorithm.
+     *
+     * <p>Valid lengths for keying material are {160, 224, 288}.
+     *
+     * <p>As per <a href="https://tools.ietf.org/html/rfc4106#section-8.1">RFC4106 (Section
+     * 8.1)</a>, keying material consists of a 128, 192, or 256 bit AES key followed by a 32-bit
+     * salt. RFC compliance requires that the salt must be unique per invocation with the same key.
+     *
+     * <p>Valid ICV (truncation) lengths are {64, 96, 128}.
+     */
+    public static final String AUTH_CRYPT_AES_GCM = "rfc4106(gcm(aes))";
+
+    /**
+     * ChaCha20-Poly1305 Authentication/Integrity + Encryption/Ciphering Algorithm.
+     *
+     * <p>Keys for this algorithm must be 288 bits in length.
+     *
+     * <p>As per <a href="https://tools.ietf.org/html/rfc7634#section-2">RFC7634 (Section 2)</a>,
+     * keying material consists of a 256 bit key followed by a 32-bit salt. The salt is fixed per
+     * security association.
+     *
+     * <p>The only valid ICV (truncation) length is 128 bits.
+     *
+     * <p>This algorithm may be available on the device. Caller MUST check if it is supported before
+     * using it by calling {@link #getSupportedAlgorithms()} and checking if this algorithm is
+     * included in the returned algorithm set. The returned algorithm set will not change unless the
+     * device is rebooted. {@link IllegalArgumentException} will be thrown if this algorithm is
+     * requested on an unsupported device.
+     *
+     * <p>@see {@link #getSupportedAlgorithms()}
+     */
+    // This algorithm may be available on devices released before Android 12, and is guaranteed
+    // to be available on devices first shipped with Android 12 or later.
+    public static final String AUTH_CRYPT_CHACHA20_POLY1305 = "rfc7539esp(chacha20,poly1305)";
+
+    /** @hide */
+    @StringDef({
+        CRYPT_AES_CBC,
+        CRYPT_AES_CTR,
+        AUTH_HMAC_MD5,
+        AUTH_HMAC_SHA1,
+        AUTH_HMAC_SHA256,
+        AUTH_HMAC_SHA384,
+        AUTH_HMAC_SHA512,
+        AUTH_AES_XCBC,
+        AUTH_AES_CMAC,
+        AUTH_CRYPT_AES_GCM,
+        AUTH_CRYPT_CHACHA20_POLY1305
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AlgorithmName {}
+
+    /** @hide */
+    @VisibleForTesting
+    public static final Map<String, Integer> ALGO_TO_REQUIRED_FIRST_SDK = new HashMap<>();
+
+    private static final int SDK_VERSION_ZERO = 0;
+
+    static {
+        ALGO_TO_REQUIRED_FIRST_SDK.put(CRYPT_AES_CBC, SDK_VERSION_ZERO);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_MD5, SDK_VERSION_ZERO);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_SHA1, SDK_VERSION_ZERO);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_SHA256, SDK_VERSION_ZERO);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_SHA384, SDK_VERSION_ZERO);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_SHA512, SDK_VERSION_ZERO);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_CRYPT_AES_GCM, SDK_VERSION_ZERO);
+
+        ALGO_TO_REQUIRED_FIRST_SDK.put(CRYPT_AES_CTR, Build.VERSION_CODES.S);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_AES_XCBC, Build.VERSION_CODES.S);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_AES_CMAC, Build.VERSION_CODES.S);
+        ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_CRYPT_CHACHA20_POLY1305, Build.VERSION_CODES.S);
+    }
+
+    private static final Set<String> ENABLED_ALGOS =
+            Collections.unmodifiableSet(loadAlgos(Resources.getSystem()));
+
+    private final String mName;
+    private final byte[] mKey;
+    private final int mTruncLenBits;
+
+    /**
+     * Creates an IpSecAlgorithm of one of the supported types. Supported algorithm names are
+     * defined as constants in this class.
+     *
+     * <p>For algorithms that produce an integrity check value, the truncation length is a required
+     * parameter. See {@link #IpSecAlgorithm(String algorithm, byte[] key, int truncLenBits)}
+     *
+     * @param algorithm name of the algorithm.
+     * @param key key padded to a multiple of 8 bits.
+     * @throws IllegalArgumentException if algorithm or key length is invalid.
+     */
+    public IpSecAlgorithm(@NonNull @AlgorithmName String algorithm, @NonNull byte[] key) {
+        this(algorithm, key, 0);
+    }
+
+    /**
+     * Creates an IpSecAlgorithm of one of the supported types. Supported algorithm names are
+     * defined as constants in this class.
+     *
+     * <p>This constructor only supports algorithms that use a truncation length. i.e.
+     * Authentication and Authenticated Encryption algorithms.
+     *
+     * @param algorithm name of the algorithm.
+     * @param key key padded to a multiple of 8 bits.
+     * @param truncLenBits number of bits of output hash to use.
+     * @throws IllegalArgumentException if algorithm, key length or truncation length is invalid.
+     */
+    public IpSecAlgorithm(
+            @NonNull @AlgorithmName String algorithm, @NonNull byte[] key, int truncLenBits) {
+        mName = algorithm;
+        mKey = key.clone();
+        mTruncLenBits = truncLenBits;
+        checkValidOrThrow(mName, mKey.length * 8, mTruncLenBits);
+    }
+
+    /** Get the algorithm name */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /** Get the key for this algorithm */
+    @NonNull
+    public byte[] getKey() {
+        return mKey.clone();
+    }
+
+    /** Get the truncation length of this algorithm, in bits */
+    public int getTruncationLengthBits() {
+        return mTruncLenBits;
+    }
+
+    /** Parcelable Implementation */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Write to parcel */
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(mName);
+        out.writeByteArray(mKey);
+        out.writeInt(mTruncLenBits);
+    }
+
+    /** Parcelable Creator */
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecAlgorithm> CREATOR =
+            new Parcelable.Creator<IpSecAlgorithm>() {
+                public IpSecAlgorithm createFromParcel(Parcel in) {
+                    final String name = in.readString();
+                    final byte[] key = in.createByteArray();
+                    final int truncLenBits = in.readInt();
+
+                    return new IpSecAlgorithm(name, key, truncLenBits);
+                }
+
+                public IpSecAlgorithm[] newArray(int size) {
+                    return new IpSecAlgorithm[size];
+                }
+            };
+
+    /**
+     * Returns supported IPsec algorithms for the current device.
+     *
+     * <p>Some algorithms may not be supported on old devices. Callers MUST check if an algorithm is
+     * supported before using it.
+     */
+    @NonNull
+    public static Set<String> getSupportedAlgorithms() {
+        return ENABLED_ALGOS;
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public static Set<String> loadAlgos(Resources systemResources) {
+        final Set<String> enabledAlgos = new HashSet<>();
+
+        // Load and validate the optional algorithm resource. Undefined or duplicate algorithms in
+        // the resource are not allowed.
+        final String[] resourceAlgos = systemResources.getStringArray(
+                android.R.array.config_optionalIpSecAlgorithms);
+        for (String str : resourceAlgos) {
+            if (!ALGO_TO_REQUIRED_FIRST_SDK.containsKey(str) || !enabledAlgos.add(str)) {
+                // This error should be caught by CTS and never be thrown to API callers
+                throw new IllegalArgumentException("Invalid or repeated algorithm " + str);
+            }
+        }
+
+        for (Entry<String, Integer> entry : ALGO_TO_REQUIRED_FIRST_SDK.entrySet()) {
+            if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= entry.getValue()) {
+                enabledAlgos.add(entry.getKey());
+            }
+        }
+
+        return enabledAlgos;
+    }
+
+    private static void checkValidOrThrow(String name, int keyLen, int truncLen) {
+        final boolean isValidLen;
+        final boolean isValidTruncLen;
+
+        if (!getSupportedAlgorithms().contains(name)) {
+            throw new IllegalArgumentException("Unsupported algorithm: " + name);
+        }
+
+        switch (name) {
+            case CRYPT_AES_CBC:
+                isValidLen = keyLen == 128 || keyLen == 192 || keyLen == 256;
+                isValidTruncLen = true;
+                break;
+            case CRYPT_AES_CTR:
+                // The keying material for AES-CTR is a key plus a 32-bit salt
+                isValidLen = keyLen == 128 + 32 || keyLen == 192 + 32 || keyLen == 256 + 32;
+                isValidTruncLen = true;
+                break;
+            case AUTH_HMAC_MD5:
+                isValidLen = keyLen == 128;
+                isValidTruncLen = truncLen >= 96 && truncLen <= 128;
+                break;
+            case AUTH_HMAC_SHA1:
+                isValidLen = keyLen == 160;
+                isValidTruncLen = truncLen >= 96 && truncLen <= 160;
+                break;
+            case AUTH_HMAC_SHA256:
+                isValidLen = keyLen == 256;
+                isValidTruncLen = truncLen >= 96 && truncLen <= 256;
+                break;
+            case AUTH_HMAC_SHA384:
+                isValidLen = keyLen == 384;
+                isValidTruncLen = truncLen >= 192 && truncLen <= 384;
+                break;
+            case AUTH_HMAC_SHA512:
+                isValidLen = keyLen == 512;
+                isValidTruncLen = truncLen >= 256 && truncLen <= 512;
+                break;
+            case AUTH_AES_XCBC:
+                isValidLen = keyLen == 128;
+                isValidTruncLen = truncLen == 96;
+                break;
+            case AUTH_AES_CMAC:
+                isValidLen = keyLen == 128;
+                isValidTruncLen = truncLen == 96;
+                break;
+            case AUTH_CRYPT_AES_GCM:
+                // The keying material for GCM is a key plus a 32-bit salt
+                isValidLen = keyLen == 128 + 32 || keyLen == 192 + 32 || keyLen == 256 + 32;
+                isValidTruncLen = truncLen == 64 || truncLen == 96 || truncLen == 128;
+                break;
+            case AUTH_CRYPT_CHACHA20_POLY1305:
+                // The keying material for ChaCha20Poly1305 is a key plus a 32-bit salt
+                isValidLen = keyLen == 256 + 32;
+                isValidTruncLen = truncLen == 128;
+                break;
+            default:
+                // Should never hit here.
+                throw new IllegalArgumentException("Couldn't find an algorithm: " + name);
+        }
+
+        if (!isValidLen) {
+            throw new IllegalArgumentException("Invalid key material keyLength: " + keyLen);
+        }
+        if (!isValidTruncLen) {
+            throw new IllegalArgumentException("Invalid truncation keyLength: " + truncLen);
+        }
+    }
+
+    /** @hide */
+    public boolean isAuthentication() {
+        switch (getName()) {
+            // Fallthrough
+            case AUTH_HMAC_MD5:
+            case AUTH_HMAC_SHA1:
+            case AUTH_HMAC_SHA256:
+            case AUTH_HMAC_SHA384:
+            case AUTH_HMAC_SHA512:
+            case AUTH_AES_XCBC:
+            case AUTH_AES_CMAC:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /** @hide */
+    public boolean isEncryption() {
+        switch (getName()) {
+            case CRYPT_AES_CBC: // fallthrough
+            case CRYPT_AES_CTR:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /** @hide */
+    public boolean isAead() {
+        switch (getName()) {
+            case AUTH_CRYPT_AES_GCM: // fallthrough
+            case AUTH_CRYPT_CHACHA20_POLY1305:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return new StringBuilder()
+                .append("{mName=")
+                .append(mName)
+                .append(", mTruncLenBits=")
+                .append(mTruncLenBits)
+                .append("}")
+                .toString();
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public static boolean equals(IpSecAlgorithm lhs, IpSecAlgorithm rhs) {
+        if (lhs == null || rhs == null) return (lhs == rhs);
+        return (lhs.mName.equals(rhs.mName)
+                && Arrays.equals(lhs.mKey, rhs.mKey)
+                && lhs.mTruncLenBits == rhs.mTruncLenBits);
+    }
+};
diff --git a/framework-t/src/android/net/IpSecConfig.aidl b/framework-t/src/android/net/IpSecConfig.aidl
new file mode 100644
index 0000000..eaefca7
--- /dev/null
+++ b/framework-t/src/android/net/IpSecConfig.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/** @hide */
+parcelable IpSecConfig;
diff --git a/framework-t/src/android/net/IpSecConfig.java b/framework-t/src/android/net/IpSecConfig.java
new file mode 100644
index 0000000..575c5ed
--- /dev/null
+++ b/framework-t/src/android/net/IpSecConfig.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * This class encapsulates all the configuration parameters needed to create IPsec transforms and
+ * policies.
+ *
+ * @hide
+ */
+public final class IpSecConfig implements Parcelable {
+    private static final String TAG = "IpSecConfig";
+
+    // MODE_TRANSPORT or MODE_TUNNEL
+    private int mMode = IpSecTransform.MODE_TRANSPORT;
+
+    // Preventing this from being null simplifies Java->Native binder
+    private String mSourceAddress = "";
+
+    // Preventing this from being null simplifies Java->Native binder
+    private String mDestinationAddress = "";
+
+    // The underlying Network that represents the "gateway" Network
+    // for outbound packets. It may also be used to select packets.
+    private Network mNetwork;
+
+    // Minimum requirements for identifying a transform
+    // SPI identifying the IPsec SA in packet processing
+    // and a destination IP address
+    private int mSpiResourceId = IpSecManager.INVALID_RESOURCE_ID;
+
+    // Encryption Algorithm
+    private IpSecAlgorithm mEncryption;
+
+    // Authentication Algorithm
+    private IpSecAlgorithm mAuthentication;
+
+    // Authenticated Encryption Algorithm
+    private IpSecAlgorithm mAuthenticatedEncryption;
+
+    // For tunnel mode IPv4 UDP Encapsulation
+    // IpSecTransform#ENCAP_ESP_*, such as ENCAP_ESP_OVER_UDP_IKE
+    private int mEncapType = IpSecTransform.ENCAP_NONE;
+    private int mEncapSocketResourceId = IpSecManager.INVALID_RESOURCE_ID;
+    private int mEncapRemotePort;
+
+    // An interval, in seconds between the NattKeepalive packets
+    private int mNattKeepaliveInterval;
+
+    // XFRM mark and mask; defaults to 0 (no mark/mask)
+    private int mMarkValue;
+    private int mMarkMask;
+
+    // XFRM interface id
+    private int mXfrmInterfaceId;
+
+    /** Set the mode for this IPsec transform */
+    public void setMode(int mode) {
+        mMode = mode;
+    }
+
+    /** Set the source IP addres for this IPsec transform */
+    public void setSourceAddress(String sourceAddress) {
+        mSourceAddress = sourceAddress;
+    }
+
+    /** Set the destination IP address for this IPsec transform */
+    public void setDestinationAddress(String destinationAddress) {
+        mDestinationAddress = destinationAddress;
+    }
+
+    /** Set the SPI by resource ID */
+    public void setSpiResourceId(int resourceId) {
+        mSpiResourceId = resourceId;
+    }
+
+    /** Set the encryption algorithm */
+    public void setEncryption(IpSecAlgorithm encryption) {
+        mEncryption = encryption;
+    }
+
+    /** Set the authentication algorithm */
+    public void setAuthentication(IpSecAlgorithm authentication) {
+        mAuthentication = authentication;
+    }
+
+    /** Set the authenticated encryption algorithm */
+    public void setAuthenticatedEncryption(IpSecAlgorithm authenticatedEncryption) {
+        mAuthenticatedEncryption = authenticatedEncryption;
+    }
+
+    /** Set the underlying network that will carry traffic for this transform */
+    public void setNetwork(Network network) {
+        mNetwork = network;
+    }
+
+    public void setEncapType(int encapType) {
+        mEncapType = encapType;
+    }
+
+    public void setEncapSocketResourceId(int resourceId) {
+        mEncapSocketResourceId = resourceId;
+    }
+
+    public void setEncapRemotePort(int port) {
+        mEncapRemotePort = port;
+    }
+
+    public void setNattKeepaliveInterval(int interval) {
+        mNattKeepaliveInterval = interval;
+    }
+
+    /**
+     * Sets the mark value
+     *
+     * <p>Internal (System server) use only. Marks passed in by users will be overwritten or
+     * ignored.
+     */
+    public void setMarkValue(int mark) {
+        mMarkValue = mark;
+    }
+
+    /**
+     * Sets the mark mask
+     *
+     * <p>Internal (System server) use only. Marks passed in by users will be overwritten or
+     * ignored.
+     */
+    public void setMarkMask(int mask) {
+        mMarkMask = mask;
+    }
+
+    public void setXfrmInterfaceId(int xfrmInterfaceId) {
+        mXfrmInterfaceId = xfrmInterfaceId;
+    }
+
+    // Transport or Tunnel
+    public int getMode() {
+        return mMode;
+    }
+
+    public String getSourceAddress() {
+        return mSourceAddress;
+    }
+
+    public int getSpiResourceId() {
+        return mSpiResourceId;
+    }
+
+    public String getDestinationAddress() {
+        return mDestinationAddress;
+    }
+
+    public IpSecAlgorithm getEncryption() {
+        return mEncryption;
+    }
+
+    public IpSecAlgorithm getAuthentication() {
+        return mAuthentication;
+    }
+
+    public IpSecAlgorithm getAuthenticatedEncryption() {
+        return mAuthenticatedEncryption;
+    }
+
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    public int getEncapType() {
+        return mEncapType;
+    }
+
+    public int getEncapSocketResourceId() {
+        return mEncapSocketResourceId;
+    }
+
+    public int getEncapRemotePort() {
+        return mEncapRemotePort;
+    }
+
+    public int getNattKeepaliveInterval() {
+        return mNattKeepaliveInterval;
+    }
+
+    public int getMarkValue() {
+        return mMarkValue;
+    }
+
+    public int getMarkMask() {
+        return mMarkMask;
+    }
+
+    public int getXfrmInterfaceId() {
+        return mXfrmInterfaceId;
+    }
+
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mMode);
+        out.writeString(mSourceAddress);
+        out.writeString(mDestinationAddress);
+        out.writeParcelable(mNetwork, flags);
+        out.writeInt(mSpiResourceId);
+        out.writeParcelable(mEncryption, flags);
+        out.writeParcelable(mAuthentication, flags);
+        out.writeParcelable(mAuthenticatedEncryption, flags);
+        out.writeInt(mEncapType);
+        out.writeInt(mEncapSocketResourceId);
+        out.writeInt(mEncapRemotePort);
+        out.writeInt(mNattKeepaliveInterval);
+        out.writeInt(mMarkValue);
+        out.writeInt(mMarkMask);
+        out.writeInt(mXfrmInterfaceId);
+    }
+
+    @VisibleForTesting
+    public IpSecConfig() {}
+
+    /** Copy constructor */
+    @VisibleForTesting
+    public IpSecConfig(IpSecConfig c) {
+        mMode = c.mMode;
+        mSourceAddress = c.mSourceAddress;
+        mDestinationAddress = c.mDestinationAddress;
+        mNetwork = c.mNetwork;
+        mSpiResourceId = c.mSpiResourceId;
+        mEncryption = c.mEncryption;
+        mAuthentication = c.mAuthentication;
+        mAuthenticatedEncryption = c.mAuthenticatedEncryption;
+        mEncapType = c.mEncapType;
+        mEncapSocketResourceId = c.mEncapSocketResourceId;
+        mEncapRemotePort = c.mEncapRemotePort;
+        mNattKeepaliveInterval = c.mNattKeepaliveInterval;
+        mMarkValue = c.mMarkValue;
+        mMarkMask = c.mMarkMask;
+        mXfrmInterfaceId = c.mXfrmInterfaceId;
+    }
+
+    private IpSecConfig(Parcel in) {
+        mMode = in.readInt();
+        mSourceAddress = in.readString();
+        mDestinationAddress = in.readString();
+        mNetwork = (Network) in.readParcelable(Network.class.getClassLoader());
+        mSpiResourceId = in.readInt();
+        mEncryption =
+                (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+        mAuthentication =
+                (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+        mAuthenticatedEncryption =
+                (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+        mEncapType = in.readInt();
+        mEncapSocketResourceId = in.readInt();
+        mEncapRemotePort = in.readInt();
+        mNattKeepaliveInterval = in.readInt();
+        mMarkValue = in.readInt();
+        mMarkMask = in.readInt();
+        mXfrmInterfaceId = in.readInt();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder strBuilder = new StringBuilder();
+        strBuilder
+                .append("{mMode=")
+                .append(mMode == IpSecTransform.MODE_TUNNEL ? "TUNNEL" : "TRANSPORT")
+                .append(", mSourceAddress=")
+                .append(mSourceAddress)
+                .append(", mDestinationAddress=")
+                .append(mDestinationAddress)
+                .append(", mNetwork=")
+                .append(mNetwork)
+                .append(", mEncapType=")
+                .append(mEncapType)
+                .append(", mEncapSocketResourceId=")
+                .append(mEncapSocketResourceId)
+                .append(", mEncapRemotePort=")
+                .append(mEncapRemotePort)
+                .append(", mNattKeepaliveInterval=")
+                .append(mNattKeepaliveInterval)
+                .append("{mSpiResourceId=")
+                .append(mSpiResourceId)
+                .append(", mEncryption=")
+                .append(mEncryption)
+                .append(", mAuthentication=")
+                .append(mAuthentication)
+                .append(", mAuthenticatedEncryption=")
+                .append(mAuthenticatedEncryption)
+                .append(", mMarkValue=")
+                .append(mMarkValue)
+                .append(", mMarkMask=")
+                .append(mMarkMask)
+                .append(", mXfrmInterfaceId=")
+                .append(mXfrmInterfaceId)
+                .append("}");
+
+        return strBuilder.toString();
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecConfig> CREATOR =
+            new Parcelable.Creator<IpSecConfig>() {
+                public IpSecConfig createFromParcel(Parcel in) {
+                    return new IpSecConfig(in);
+                }
+
+                public IpSecConfig[] newArray(int size) {
+                    return new IpSecConfig[size];
+                }
+            };
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (!(other instanceof IpSecConfig)) return false;
+        final IpSecConfig rhs = (IpSecConfig) other;
+        return (mMode == rhs.mMode
+                && mSourceAddress.equals(rhs.mSourceAddress)
+                && mDestinationAddress.equals(rhs.mDestinationAddress)
+                && ((mNetwork != null && mNetwork.equals(rhs.mNetwork))
+                        || (mNetwork == rhs.mNetwork))
+                && mEncapType == rhs.mEncapType
+                && mEncapSocketResourceId == rhs.mEncapSocketResourceId
+                && mEncapRemotePort == rhs.mEncapRemotePort
+                && mNattKeepaliveInterval == rhs.mNattKeepaliveInterval
+                && mSpiResourceId == rhs.mSpiResourceId
+                && IpSecAlgorithm.equals(mEncryption, rhs.mEncryption)
+                && IpSecAlgorithm.equals(mAuthenticatedEncryption, rhs.mAuthenticatedEncryption)
+                && IpSecAlgorithm.equals(mAuthentication, rhs.mAuthentication)
+                && mMarkValue == rhs.mMarkValue
+                && mMarkMask == rhs.mMarkMask
+                && mXfrmInterfaceId == rhs.mXfrmInterfaceId);
+    }
+}
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
new file mode 100644
index 0000000..9cb0947
--- /dev/null
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -0,0 +1,1065 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.AndroidException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.system.CloseGuard;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.Objects;
+
+/**
+ * This class contains methods for managing IPsec sessions. Once configured, the kernel will apply
+ * confidentiality (encryption) and integrity (authentication) to IP traffic.
+ *
+ * <p>Note that not all aspects of IPsec are permitted by this API. Applications may create
+ * transport mode security associations and apply them to individual sockets. Applications looking
+ * to create an IPsec VPN should use {@link VpnManager} and {@link Ikev2VpnProfile}.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
+ *     Internet Protocol</a>
+ */
+@SystemService(Context.IPSEC_SERVICE)
+public class IpSecManager {
+    private static final String TAG = "IpSecManager";
+
+    /**
+     * Used when applying a transform to direct traffic through an {@link IpSecTransform}
+     * towards the host.
+     *
+     * <p>See {@link #applyTransportModeTransform(Socket, int, IpSecTransform)}.
+     */
+    public static final int DIRECTION_IN = 0;
+
+    /**
+     * Used when applying a transform to direct traffic through an {@link IpSecTransform}
+     * away from the host.
+     *
+     * <p>See {@link #applyTransportModeTransform(Socket, int, IpSecTransform)}.
+     */
+    public static final int DIRECTION_OUT = 1;
+
+    /**
+     * Used when applying a transform to direct traffic through an {@link IpSecTransform} for
+     * forwarding between interfaces.
+     *
+     * <p>See {@link #applyTransportModeTransform(Socket, int, IpSecTransform)}.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int DIRECTION_FWD = 2;
+
+    /** @hide */
+    @IntDef(value = {DIRECTION_IN, DIRECTION_OUT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PolicyDirection {}
+
+    /**
+     * The Security Parameter Index (SPI) 0 indicates an unknown or invalid index.
+     *
+     * <p>No IPsec packet may contain an SPI of 0.
+     *
+     * @hide
+     */
+    @TestApi public static final int INVALID_SECURITY_PARAMETER_INDEX = 0;
+
+    /** @hide */
+    public interface Status {
+        int OK = 0;
+        int RESOURCE_UNAVAILABLE = 1;
+        int SPI_UNAVAILABLE = 2;
+    }
+
+    /** @hide */
+    public static final int INVALID_RESOURCE_ID = -1;
+
+    /**
+     * Thrown to indicate that a requested SPI is in use.
+     *
+     * <p>The combination of remote {@code InetAddress} and SPI must be unique across all apps on
+     * one device. If this error is encountered, a new SPI is required before a transform may be
+     * created. This error can be avoided by calling {@link
+     * IpSecManager#allocateSecurityParameterIndex}.
+     */
+    public static final class SpiUnavailableException extends AndroidException {
+        private final int mSpi;
+
+        /**
+         * Construct an exception indicating that a transform with the given SPI is already in use
+         * or otherwise unavailable.
+         *
+         * @param msg description indicating the colliding SPI
+         * @param spi the SPI that could not be used due to a collision
+         */
+        SpiUnavailableException(String msg, int spi) {
+            super(msg + " (spi: " + spi + ")");
+            mSpi = spi;
+        }
+
+        /** Get the SPI that caused a collision. */
+        public int getSpi() {
+            return mSpi;
+        }
+    }
+
+    /**
+     * Thrown to indicate that an IPsec resource is unavailable.
+     *
+     * <p>This could apply to resources such as sockets, {@link SecurityParameterIndex}, {@link
+     * IpSecTransform}, or other system resources. If this exception is thrown, users should release
+     * allocated objects of the type requested.
+     */
+    public static final class ResourceUnavailableException extends AndroidException {
+
+        ResourceUnavailableException(String msg) {
+            super(msg);
+        }
+    }
+
+    private final Context mContext;
+    private final IIpSecService mService;
+
+    /**
+     * This class represents a reserved SPI.
+     *
+     * <p>Objects of this type are used to track reserved security parameter indices. They can be
+     * obtained by calling {@link IpSecManager#allocateSecurityParameterIndex} and must be released
+     * by calling {@link #close()} when they are no longer needed.
+     */
+    public static final class SecurityParameterIndex implements AutoCloseable {
+        private final IIpSecService mService;
+        private final InetAddress mDestinationAddress;
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+        private int mSpi = INVALID_SECURITY_PARAMETER_INDEX;
+        private int mResourceId = INVALID_RESOURCE_ID;
+
+        /** Get the underlying SPI held by this object. */
+        public int getSpi() {
+            return mSpi;
+        }
+
+        /**
+         * Release an SPI that was previously reserved.
+         *
+         * <p>Release an SPI for use by other users in the system. If a SecurityParameterIndex is
+         * applied to an IpSecTransform, it will become unusable for future transforms but should
+         * still be closed to ensure system resources are released.
+         */
+        @Override
+        public void close() {
+            try {
+                mService.releaseSecurityParameterIndex(mResourceId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } catch (Exception e) {
+                // On close we swallow all random exceptions since failure to close is not
+                // actionable by the user.
+                Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+            } finally {
+                mResourceId = INVALID_RESOURCE_ID;
+                mCloseGuard.close();
+            }
+        }
+
+        /** Check that the SPI was closed properly. */
+        @Override
+        protected void finalize() throws Throwable {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+
+            close();
+        }
+
+        private SecurityParameterIndex(
+                @NonNull IIpSecService service, InetAddress destinationAddress, int spi)
+                throws ResourceUnavailableException, SpiUnavailableException {
+            mService = service;
+            mDestinationAddress = destinationAddress;
+            try {
+                IpSecSpiResponse result =
+                        mService.allocateSecurityParameterIndex(
+                                destinationAddress.getHostAddress(), spi, new Binder());
+
+                if (result == null) {
+                    throw new NullPointerException("Received null response from IpSecService");
+                }
+
+                int status = result.status;
+                switch (status) {
+                    case Status.OK:
+                        break;
+                    case Status.RESOURCE_UNAVAILABLE:
+                        throw new ResourceUnavailableException(
+                                "No more SPIs may be allocated by this requester.");
+                    case Status.SPI_UNAVAILABLE:
+                        throw new SpiUnavailableException("Requested SPI is unavailable", spi);
+                    default:
+                        throw new RuntimeException(
+                                "Unknown status returned by IpSecService: " + status);
+                }
+                mSpi = result.spi;
+                mResourceId = result.resourceId;
+
+                if (mSpi == INVALID_SECURITY_PARAMETER_INDEX) {
+                    throw new RuntimeException("Invalid SPI returned by IpSecService: " + status);
+                }
+
+                if (mResourceId == INVALID_RESOURCE_ID) {
+                    throw new RuntimeException(
+                            "Invalid Resource ID returned by IpSecService: " + status);
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mCloseGuard.open("open");
+        }
+
+        /** @hide */
+        @VisibleForTesting
+        public int getResourceId() {
+            return mResourceId;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                .append("SecurityParameterIndex{spi=")
+                .append(mSpi)
+                .append(",resourceId=")
+                .append(mResourceId)
+                .append("}")
+                .toString();
+        }
+    }
+
+    /**
+     * Reserve a random SPI for traffic bound to or from the specified destination address.
+     *
+     * <p>If successful, this SPI is guaranteed available until released by a call to {@link
+     * SecurityParameterIndex#close()}.
+     *
+     * @param destinationAddress the destination address for traffic bearing the requested SPI.
+     *     For inbound traffic, the destination should be an address currently assigned on-device.
+     * @return the reserved SecurityParameterIndex
+     * @throws ResourceUnavailableException indicating that too many SPIs are
+     *     currently allocated for this user
+     */
+    @NonNull
+    public SecurityParameterIndex allocateSecurityParameterIndex(
+                @NonNull InetAddress destinationAddress) throws ResourceUnavailableException {
+        try {
+            return new SecurityParameterIndex(
+                    mService,
+                    destinationAddress,
+                    IpSecManager.INVALID_SECURITY_PARAMETER_INDEX);
+        } catch (ServiceSpecificException e) {
+            throw rethrowUncheckedExceptionFromServiceSpecificException(e);
+        } catch (SpiUnavailableException unlikely) {
+            // Because this function allocates a totally random SPI, it really shouldn't ever
+            // fail to allocate an SPI; we simply need this because the exception is checked.
+            throw new ResourceUnavailableException("No SPIs available");
+        }
+    }
+
+    /**
+     * Reserve the requested SPI for traffic bound to or from the specified destination address.
+     *
+     * <p>If successful, this SPI is guaranteed available until released by a call to {@link
+     * SecurityParameterIndex#close()}.
+     *
+     * @param destinationAddress the destination address for traffic bearing the requested SPI.
+     *     For inbound traffic, the destination should be an address currently assigned on-device.
+     * @param requestedSpi the requested SPI. The range 1-255 is reserved and may not be used. See
+     *     RFC 4303 Section 2.1.
+     * @return the reserved SecurityParameterIndex
+     * @throws ResourceUnavailableException indicating that too many SPIs are
+     *     currently allocated for this user
+     * @throws SpiUnavailableException indicating that the requested SPI could not be
+     *     reserved
+     */
+    @NonNull
+    public SecurityParameterIndex allocateSecurityParameterIndex(
+            @NonNull InetAddress destinationAddress, int requestedSpi)
+            throws SpiUnavailableException, ResourceUnavailableException {
+        if (requestedSpi == IpSecManager.INVALID_SECURITY_PARAMETER_INDEX) {
+            throw new IllegalArgumentException("Requested SPI must be a valid (non-zero) SPI");
+        }
+        try {
+            return new SecurityParameterIndex(mService, destinationAddress, requestedSpi);
+        } catch (ServiceSpecificException e) {
+            throw rethrowUncheckedExceptionFromServiceSpecificException(e);
+        }
+    }
+
+    /**
+     * Apply an IPsec transform to a stream socket.
+     *
+     * <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
+     * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
+     * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
+     * unprotected traffic can resume on that socket.
+     *
+     * <p>For security reasons, the destination address of any traffic on the socket must match the
+     * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
+     * other IP address will result in an IOException. In addition, reads and writes on the socket
+     * will throw IOException if the user deactivates the transform (by calling {@link
+     * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
+     *
+     * <p>Note that when applied to TCP sockets, calling {@link IpSecTransform#close()} on an
+     * applied transform before completion of graceful shutdown may result in the shutdown sequence
+     * failing to complete. As such, applications requiring graceful shutdown MUST close the socket
+     * prior to deactivating the applied transform. Socket closure may be performed asynchronously
+     * (in batches), so the returning of a close function does not guarantee shutdown of a socket.
+     * Setting an SO_LINGER timeout results in socket closure being performed synchronously, and is
+     * sufficient to ensure shutdown.
+     *
+     * Specifically, if the transform is deactivated (by calling {@link IpSecTransform#close()}),
+     * prior to the socket being closed, the standard [FIN - FIN/ACK - ACK], or the reset [RST]
+     * packets are dropped due to the lack of a valid Transform. Similarly, if a socket without the
+     * SO_LINGER option set is closed, the delayed/batched FIN packets may be dropped.
+     *
+     * <h4>Rekey Procedure</h4>
+     *
+     * <p>When applying a new tranform to a socket in the outbound direction, the previous transform
+     * will be removed and the new transform will take effect immediately, sending all traffic on
+     * the new transform; however, when applying a transform in the inbound direction, traffic
+     * on the old transform will continue to be decrypted and delivered until that transform is
+     * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey
+     * procedures where both transforms are valid until both endpoints are using the new transform
+     * and all in-flight packets have been received.
+     *
+     * @param socket a stream socket
+     * @param direction the direction in which the transform should be applied
+     * @param transform a transport mode {@code IpSecTransform}
+     * @throws IOException indicating that the transform could not be applied
+     */
+    public void applyTransportModeTransform(@NonNull Socket socket,
+            @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        // Ensure creation of FD. See b/77548890 for more details.
+        socket.getSoLinger();
+
+        applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
+    }
+
+    /**
+     * Apply an IPsec transform to a datagram socket.
+     *
+     * <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
+     * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
+     * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
+     * unprotected traffic can resume on that socket.
+     *
+     * <p>For security reasons, the destination address of any traffic on the socket must match the
+     * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
+     * other IP address will result in an IOException. In addition, reads and writes on the socket
+     * will throw IOException if the user deactivates the transform (by calling {@link
+     * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
+     *
+     * <h4>Rekey Procedure</h4>
+     *
+     * <p>When applying a new tranform to a socket in the outbound direction, the previous transform
+     * will be removed and the new transform will take effect immediately, sending all traffic on
+     * the new transform; however, when applying a transform in the inbound direction, traffic
+     * on the old transform will continue to be decrypted and delivered until that transform is
+     * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey
+     * procedures where both transforms are valid until both endpoints are using the new transform
+     * and all in-flight packets have been received.
+     *
+     * @param socket a datagram socket
+     * @param direction the direction in which the transform should be applied
+     * @param transform a transport mode {@code IpSecTransform}
+     * @throws IOException indicating that the transform could not be applied
+     */
+    public void applyTransportModeTransform(@NonNull DatagramSocket socket,
+            @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
+    }
+
+    /**
+     * Apply an IPsec transform to a socket.
+     *
+     * <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
+     * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
+     * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
+     * unprotected traffic can resume on that socket.
+     *
+     * <p>For security reasons, the destination address of any traffic on the socket must match the
+     * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
+     * other IP address will result in an IOException. In addition, reads and writes on the socket
+     * will throw IOException if the user deactivates the transform (by calling {@link
+     * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
+     *
+     * <p>Note that when applied to TCP sockets, calling {@link IpSecTransform#close()} on an
+     * applied transform before completion of graceful shutdown may result in the shutdown sequence
+     * failing to complete. As such, applications requiring graceful shutdown MUST close the socket
+     * prior to deactivating the applied transform. Socket closure may be performed asynchronously
+     * (in batches), so the returning of a close function does not guarantee shutdown of a socket.
+     * Setting an SO_LINGER timeout results in socket closure being performed synchronously, and is
+     * sufficient to ensure shutdown.
+     *
+     * Specifically, if the transform is deactivated (by calling {@link IpSecTransform#close()}),
+     * prior to the socket being closed, the standard [FIN - FIN/ACK - ACK], or the reset [RST]
+     * packets are dropped due to the lack of a valid Transform. Similarly, if a socket without the
+     * SO_LINGER option set is closed, the delayed/batched FIN packets may be dropped.
+     *
+     * <h4>Rekey Procedure</h4>
+     *
+     * <p>When applying a new tranform to a socket in the outbound direction, the previous transform
+     * will be removed and the new transform will take effect immediately, sending all traffic on
+     * the new transform; however, when applying a transform in the inbound direction, traffic
+     * on the old transform will continue to be decrypted and delivered until that transform is
+     * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey
+     * procedures where both transforms are valid until both endpoints are using the new transform
+     * and all in-flight packets have been received.
+     *
+     * @param socket a socket file descriptor
+     * @param direction the direction in which the transform should be applied
+     * @param transform a transport mode {@code IpSecTransform}
+     * @throws IOException indicating that the transform could not be applied
+     */
+    public void applyTransportModeTransform(@NonNull FileDescriptor socket,
+            @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        // We dup() the FileDescriptor here because if we don't, then the ParcelFileDescriptor()
+        // constructor takes control and closes the user's FD when we exit the method.
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
+            mService.applyTransportModeTransform(pfd, direction, transform.getResourceId());
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove an IPsec transform from a stream socket.
+     *
+     * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+     * socket allows the socket to be reused for communication in the clear.
+     *
+     * <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
+     * {@link IpSecTransform#close()}, then communication on the socket will fail until this method
+     * is called.
+     *
+     * @param socket a socket that previously had a transform applied to it
+     * @throws IOException indicating that the transform could not be removed from the socket
+     */
+    public void removeTransportModeTransforms(@NonNull Socket socket) throws IOException {
+        // Ensure creation of FD. See b/77548890 for more details.
+        socket.getSoLinger();
+
+        removeTransportModeTransforms(socket.getFileDescriptor$());
+    }
+
+    /**
+     * Remove an IPsec transform from a datagram socket.
+     *
+     * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+     * socket allows the socket to be reused for communication in the clear.
+     *
+     * <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
+     * {@link IpSecTransform#close()}, then communication on the socket will fail until this method
+     * is called.
+     *
+     * @param socket a socket that previously had a transform applied to it
+     * @throws IOException indicating that the transform could not be removed from the socket
+     */
+    public void removeTransportModeTransforms(@NonNull DatagramSocket socket) throws IOException {
+        removeTransportModeTransforms(socket.getFileDescriptor$());
+    }
+
+    /**
+     * Remove an IPsec transform from a socket.
+     *
+     * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+     * socket allows the socket to be reused for communication in the clear.
+     *
+     * <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
+     * {@link IpSecTransform#close()}, then communication on the socket will fail until this method
+     * is called.
+     *
+     * @param socket a socket that previously had a transform applied to it
+     * @throws IOException indicating that the transform could not be removed from the socket
+     */
+    public void removeTransportModeTransforms(@NonNull FileDescriptor socket) throws IOException {
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
+            mService.removeTransportModeTransforms(pfd);
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove a Tunnel Mode IPsec Transform from a {@link Network}. This must be used as part of
+     * cleanup if a tunneled Network experiences a change in default route. The Network will drop
+     * all traffic that cannot be routed to the Tunnel's outbound interface. If that interface is
+     * lost, all traffic will drop.
+     *
+     * <p>TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked.
+     *
+     * @param net a network that currently has transform applied to it.
+     * @param transform a Tunnel Mode IPsec Transform that has been previously applied to the given
+     *     network
+     * @hide
+     */
+    public void removeTunnelModeTransform(Network net, IpSecTransform transform) {}
+
+    /**
+     * This class provides access to a UDP encapsulation Socket.
+     *
+     * <p>{@code UdpEncapsulationSocket} wraps a system-provided datagram socket intended for IKEv2
+     * signalling and UDP encapsulated IPsec traffic. Instances can be obtained by calling {@link
+     * IpSecManager#openUdpEncapsulationSocket}. The provided socket cannot be re-bound by the
+     * caller. The caller should not close the {@code FileDescriptor} returned by {@link
+     * #getFileDescriptor}, but should use {@link #close} instead.
+     *
+     * <p>Allowing the user to close or unbind a UDP encapsulation socket could impact the traffic
+     * of the next user who binds to that port. To prevent this scenario, these sockets are held
+     * open by the system so that they may only be closed by calling {@link #close} or when the user
+     * process exits.
+     */
+    public static final class UdpEncapsulationSocket implements AutoCloseable {
+        private final ParcelFileDescriptor mPfd;
+        private final IIpSecService mService;
+        private int mResourceId = INVALID_RESOURCE_ID;
+        private final int mPort;
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+
+        private UdpEncapsulationSocket(@NonNull IIpSecService service, int port)
+                throws ResourceUnavailableException, IOException {
+            mService = service;
+            try {
+                IpSecUdpEncapResponse result =
+                        mService.openUdpEncapsulationSocket(port, new Binder());
+                switch (result.status) {
+                    case Status.OK:
+                        break;
+                    case Status.RESOURCE_UNAVAILABLE:
+                        throw new ResourceUnavailableException(
+                                "No more Sockets may be allocated by this requester.");
+                    default:
+                        throw new RuntimeException(
+                                "Unknown status returned by IpSecService: " + result.status);
+                }
+                mResourceId = result.resourceId;
+                mPort = result.port;
+                mPfd = result.fileDescriptor;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mCloseGuard.open("constructor");
+        }
+
+        /** Get the encapsulation socket's file descriptor. */
+        public FileDescriptor getFileDescriptor() {
+            if (mPfd == null) {
+                return null;
+            }
+            return mPfd.getFileDescriptor();
+        }
+
+        /** Get the bound port of the wrapped socket. */
+        public int getPort() {
+            return mPort;
+        }
+
+        /**
+         * Close this socket.
+         *
+         * <p>This closes the wrapped socket. Open encapsulation sockets count against a user's
+         * resource limits, and forgetting to close them eventually will result in {@link
+         * ResourceUnavailableException} being thrown.
+         */
+        @Override
+        public void close() throws IOException {
+            try {
+                mService.closeUdpEncapsulationSocket(mResourceId);
+                mResourceId = INVALID_RESOURCE_ID;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } catch (Exception e) {
+                // On close we swallow all random exceptions since failure to close is not
+                // actionable by the user.
+                Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+            } finally {
+                mResourceId = INVALID_RESOURCE_ID;
+                mCloseGuard.close();
+            }
+
+            try {
+                mPfd.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to close UDP Encapsulation Socket with Port= " + mPort);
+                throw e;
+            }
+        }
+
+        /** Check that the socket was closed properly. */
+        @Override
+        protected void finalize() throws Throwable {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        }
+
+        /** @hide */
+        @SystemApi(client = MODULE_LIBRARIES)
+        public int getResourceId() {
+            return mResourceId;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                .append("UdpEncapsulationSocket{port=")
+                .append(mPort)
+                .append(",resourceId=")
+                .append(mResourceId)
+                .append("}")
+                .toString();
+        }
+    };
+
+    /**
+     * Open a socket for UDP encapsulation and bind to the given port.
+     *
+     * <p>See {@link UdpEncapsulationSocket} for the proper way to close the returned socket.
+     *
+     * @param port a local UDP port
+     * @return a socket that is bound to the given port
+     * @throws IOException indicating that the socket could not be opened or bound
+     * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
+     */
+    // Returning a socket in this fashion that has been created and bound by the system
+    // is the only safe way to ensure that a socket is both accessible to the user and
+    // safely usable for Encapsulation without allowing a user to possibly unbind from/close
+    // the port, which could potentially impact the traffic of the next user who binds to that
+    // socket.
+    @NonNull
+    public UdpEncapsulationSocket openUdpEncapsulationSocket(int port)
+            throws IOException, ResourceUnavailableException {
+        /*
+         * Most range checking is done in the service, but this version of the constructor expects
+         * a valid port number, and zero cannot be checked after being passed to the service.
+         */
+        if (port == 0) {
+            throw new IllegalArgumentException("Specified port must be a valid port number!");
+        }
+        try {
+            return new UdpEncapsulationSocket(mService, port);
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        }
+    }
+
+    /**
+     * Open a socket for UDP encapsulation.
+     *
+     * <p>See {@link UdpEncapsulationSocket} for the proper way to close the returned socket.
+     *
+     * <p>The local port of the returned socket can be obtained by calling {@link
+     * UdpEncapsulationSocket#getPort()}.
+     *
+     * @return a socket that is bound to a local port
+     * @throws IOException indicating that the socket could not be opened or bound
+     * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
+     */
+    // Returning a socket in this fashion that has been created and bound by the system
+    // is the only safe way to ensure that a socket is both accessible to the user and
+    // safely usable for Encapsulation without allowing a user to possibly unbind from/close
+    // the port, which could potentially impact the traffic of the next user who binds to that
+    // socket.
+    @NonNull
+    public UdpEncapsulationSocket openUdpEncapsulationSocket()
+            throws IOException, ResourceUnavailableException {
+        try {
+            return new UdpEncapsulationSocket(mService, 0);
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        }
+    }
+
+    /**
+     * This class represents an IpSecTunnelInterface
+     *
+     * <p>IpSecTunnelInterface objects track tunnel interfaces that serve as
+     * local endpoints for IPsec tunnels.
+     *
+     * <p>Creating an IpSecTunnelInterface creates a device to which IpSecTransforms may be
+     * applied to provide IPsec security to packets sent through the tunnel. While a tunnel
+     * cannot be used in standalone mode within Android, the higher layers may use the tunnel
+     * to create Network objects which are accessible to the Android system.
+     * @hide
+     */
+    @SystemApi
+    public static final class IpSecTunnelInterface implements AutoCloseable {
+        private final String mOpPackageName;
+        private final IIpSecService mService;
+        private final InetAddress mRemoteAddress;
+        private final InetAddress mLocalAddress;
+        private final Network mUnderlyingNetwork;
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+        private String mInterfaceName;
+        private int mResourceId = INVALID_RESOURCE_ID;
+
+        /** Get the underlying SPI held by this object. */
+        @NonNull
+        public String getInterfaceName() {
+            return mInterfaceName;
+        }
+
+        /**
+         * Add an address to the IpSecTunnelInterface
+         *
+         * <p>Add an address which may be used as the local inner address for
+         * tunneled traffic.
+         *
+         * @param address the local address for traffic inside the tunnel
+         * @param prefixLen length of the InetAddress prefix
+         * @hide
+         */
+        @SystemApi
+        @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+        @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+        public void addAddress(@NonNull InetAddress address, int prefixLen) throws IOException {
+            try {
+                mService.addAddressToTunnelInterface(
+                        mResourceId, new LinkAddress(address, prefixLen), mOpPackageName);
+            } catch (ServiceSpecificException e) {
+                throw rethrowCheckedExceptionFromServiceSpecificException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Remove an address from the IpSecTunnelInterface
+         *
+         * <p>Remove an address which was previously added to the IpSecTunnelInterface
+         *
+         * @param address to be removed
+         * @param prefixLen length of the InetAddress prefix
+         * @hide
+         */
+        @SystemApi
+        @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+        @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+        public void removeAddress(@NonNull InetAddress address, int prefixLen) throws IOException {
+            try {
+                mService.removeAddressFromTunnelInterface(
+                        mResourceId, new LinkAddress(address, prefixLen), mOpPackageName);
+            } catch (ServiceSpecificException e) {
+                throw rethrowCheckedExceptionFromServiceSpecificException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Update the underlying network for this IpSecTunnelInterface.
+         *
+         * <p>This new underlying network will be used for all transforms applied AFTER this call is
+         * complete. Before new {@link IpSecTransform}(s) with matching addresses are applied to
+         * this tunnel interface, traffic will still use the old SA, and be routed on the old
+         * underlying network.
+         *
+         * <p>To migrate IPsec tunnel mode traffic, a caller should:
+         *
+         * <ol>
+         *   <li>Update the IpSecTunnelInterface’s underlying network.
+         *   <li>Apply {@link IpSecTransform}(s) with matching addresses to this
+         *       IpSecTunnelInterface.
+         * </ol>
+         *
+         * @param underlyingNetwork the new {@link Network} that will carry traffic for this tunnel.
+         *     This network MUST never be the network exposing this IpSecTunnelInterface, otherwise
+         *     this method will throw an {@link IllegalArgumentException}. If the
+         *     IpSecTunnelInterface is later added to this network, all outbound traffic will be
+         *     blackholed.
+         */
+        // TODO: b/169171001 Update the documentation when transform migration is supported.
+        // The purpose of making updating network and applying transforms separate is to leave open
+        // the possibility to support lossless migration procedures. To do that, Android platform
+        // will need to support multiple inbound tunnel mode transforms, just like it can support
+        // multiple transport mode transforms.
+        @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+        @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+        public void setUnderlyingNetwork(@NonNull Network underlyingNetwork) throws IOException {
+            try {
+                mService.setNetworkForTunnelInterface(
+                        mResourceId, underlyingNetwork, mOpPackageName);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        private IpSecTunnelInterface(@NonNull Context ctx, @NonNull IIpSecService service,
+                @NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress,
+                @NonNull Network underlyingNetwork)
+                throws ResourceUnavailableException, IOException {
+            mOpPackageName = ctx.getOpPackageName();
+            mService = service;
+            mLocalAddress = localAddress;
+            mRemoteAddress = remoteAddress;
+            mUnderlyingNetwork = underlyingNetwork;
+
+            try {
+                IpSecTunnelInterfaceResponse result =
+                        mService.createTunnelInterface(
+                                localAddress.getHostAddress(),
+                                remoteAddress.getHostAddress(),
+                                underlyingNetwork,
+                                new Binder(),
+                                mOpPackageName);
+                switch (result.status) {
+                    case Status.OK:
+                        break;
+                    case Status.RESOURCE_UNAVAILABLE:
+                        throw new ResourceUnavailableException(
+                                "No more tunnel interfaces may be allocated by this requester.");
+                    default:
+                        throw new RuntimeException(
+                                "Unknown status returned by IpSecService: " + result.status);
+                }
+                mResourceId = result.resourceId;
+                mInterfaceName = result.interfaceName;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mCloseGuard.open("constructor");
+        }
+
+        /**
+         * Delete an IpSecTunnelInterface
+         *
+         * <p>Calling close will deallocate the IpSecTunnelInterface and all of its system
+         * resources. Any packets bound for this interface either inbound or outbound will
+         * all be lost.
+         */
+        @Override
+        public void close() {
+            try {
+                mService.deleteTunnelInterface(mResourceId, mOpPackageName);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } catch (Exception e) {
+                // On close we swallow all random exceptions since failure to close is not
+                // actionable by the user.
+                Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+            } finally {
+                mResourceId = INVALID_RESOURCE_ID;
+                mCloseGuard.close();
+            }
+        }
+
+        /** Check that the Interface was closed properly. */
+        @Override
+        protected void finalize() throws Throwable {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        }
+
+        /** @hide */
+        @VisibleForTesting
+        public int getResourceId() {
+            return mResourceId;
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                .append("IpSecTunnelInterface{ifname=")
+                .append(mInterfaceName)
+                .append(",resourceId=")
+                .append(mResourceId)
+                .append("}")
+                .toString();
+        }
+    }
+
+    /**
+     * Create a new IpSecTunnelInterface as a local endpoint for tunneled IPsec traffic.
+     *
+     * <p>An application that creates tunnels is responsible for cleaning up the tunnel when the
+     * underlying network goes away, and the onLost() callback is received.
+     *
+     * @param localAddress The local addres of the tunnel
+     * @param remoteAddress The local addres of the tunnel
+     * @param underlyingNetwork the {@link Network} that will carry traffic for this tunnel.
+     *        This network should almost certainly be a network such as WiFi with an L2 address.
+     * @return a new {@link IpSecManager#IpSecTunnelInterface} with the specified properties
+     * @throws IOException indicating that the socket could not be opened or bound
+     * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+    @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+    public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress,
+            @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork)
+            throws ResourceUnavailableException, IOException {
+        try {
+            return new IpSecTunnelInterface(
+                    mContext, mService, localAddress, remoteAddress, underlyingNetwork);
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        }
+    }
+
+    /**
+     * Apply an active Tunnel Mode IPsec Transform to a {@link IpSecTunnelInterface}, which will
+     * tunnel all traffic for the given direction through the underlying network's interface with
+     * IPsec (applies an outer IP header and IPsec Header to all traffic, and expects an additional
+     * IP header and IPsec Header on all inbound traffic).
+     * <p>Applications should probably not use this API directly.
+     *
+     *
+     * @param tunnel The {@link IpSecManager#IpSecTunnelInterface} that will use the supplied
+     *        transform.
+     * @param direction the direction, {@link DIRECTION_OUT} or {@link #DIRECTION_IN} in which
+     *        the transform will be used.
+     * @param transform an {@link IpSecTransform} created in tunnel mode
+     * @throws IOException indicating that the transform could not be applied due to a lower
+     *         layer failure.
+     * @hide
+     */
+    @SystemApi
+    @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+    @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+    public void applyTunnelModeTransform(@NonNull IpSecTunnelInterface tunnel,
+            @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        try {
+            mService.applyTunnelModeTransform(
+                    tunnel.getResourceId(), direction,
+                    transform.getResourceId(), mContext.getOpPackageName());
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public IpSecTransformResponse createTransform(IpSecConfig config, IBinder binder,
+            String callingPackage) {
+        try {
+            return mService.createTransform(config, binder, callingPackage);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void deleteTransform(int resourceId) {
+        try {
+            mService.deleteTransform(resourceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Construct an instance of IpSecManager within an application context.
+     *
+     * @param context the application context for this manager
+     * @hide
+     */
+    public IpSecManager(Context ctx, IIpSecService service) {
+        mContext = ctx;
+        mService = Objects.requireNonNull(service, "missing service");
+    }
+
+    private static void maybeHandleServiceSpecificException(ServiceSpecificException sse) {
+        // OsConstants are late binding, so switch statements can't be used.
+        if (sse.errorCode == OsConstants.EINVAL) {
+            throw new IllegalArgumentException(sse);
+        } else if (sse.errorCode == OsConstants.EAGAIN) {
+            throw new IllegalStateException(sse);
+        } else if (sse.errorCode == OsConstants.EOPNOTSUPP
+                || sse.errorCode == OsConstants.EPROTONOSUPPORT) {
+            throw new UnsupportedOperationException(sse);
+        }
+    }
+
+    /**
+     * Convert an Errno SSE to the correct Unchecked exception type.
+     *
+     * This method never actually returns.
+     */
+    // package
+    static RuntimeException
+            rethrowUncheckedExceptionFromServiceSpecificException(ServiceSpecificException sse) {
+        maybeHandleServiceSpecificException(sse);
+        throw new RuntimeException(sse);
+    }
+
+    /**
+     * Convert an Errno SSE to the correct Checked or Unchecked exception type.
+     *
+     * This method may throw IOException, or it may throw an unchecked exception; it will never
+     * actually return.
+     */
+    // package
+    static IOException rethrowCheckedExceptionFromServiceSpecificException(
+            ServiceSpecificException sse) throws IOException {
+        // First see if this is an unchecked exception of a type we know.
+        // If so, then we prefer the unchecked (specific) type of exception.
+        maybeHandleServiceSpecificException(sse);
+        // If not, then all we can do is provide the SSE in the form of an IOException.
+        throw new ErrnoException(
+                "IpSec encountered errno=" + sse.errorCode, sse.errorCode).rethrowAsIOException();
+    }
+}
diff --git a/framework-t/src/android/net/IpSecSpiResponse.aidl b/framework-t/src/android/net/IpSecSpiResponse.aidl
new file mode 100644
index 0000000..6484a00
--- /dev/null
+++ b/framework-t/src/android/net/IpSecSpiResponse.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/** @hide */
+parcelable IpSecSpiResponse;
diff --git a/framework-t/src/android/net/IpSecSpiResponse.java b/framework-t/src/android/net/IpSecSpiResponse.java
new file mode 100644
index 0000000..f99e570
--- /dev/null
+++ b/framework-t/src/android/net/IpSecSpiResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an SPI and corresponding status from the IpSecService to an
+ * IpSecManager.SecurityParameterIndex.
+ *
+ * @hide
+ */
+public final class IpSecSpiResponse implements Parcelable {
+    private static final String TAG = "IpSecSpiResponse";
+
+    public final int resourceId;
+    public final int status;
+    public final int spi;
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+        out.writeInt(spi);
+    }
+
+    public IpSecSpiResponse(int inStatus, int inResourceId, int inSpi) {
+        status = inStatus;
+        resourceId = inResourceId;
+        spi = inSpi;
+    }
+
+    public IpSecSpiResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+        spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
+    }
+
+    private IpSecSpiResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+        spi = in.readInt();
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecSpiResponse> CREATOR =
+            new Parcelable.Creator<IpSecSpiResponse>() {
+                public IpSecSpiResponse createFromParcel(Parcel in) {
+                    return new IpSecSpiResponse(in);
+                }
+
+                public IpSecSpiResponse[] newArray(int size) {
+                    return new IpSecSpiResponse[size];
+                }
+            };
+}
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
new file mode 100644
index 0000000..68ae5de
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import static android.net.IpSecManager.INVALID_RESOURCE_ID;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.system.CloseGuard;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.util.Objects;
+
+/**
+ * This class represents a transform, which roughly corresponds to an IPsec Security Association.
+ *
+ * <p>Transforms are created using {@link IpSecTransform.Builder}. Each {@code IpSecTransform}
+ * object encapsulates the properties and state of an IPsec security association. That includes,
+ * but is not limited to, algorithm choice, key material, and allocated system resources.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
+ *     Internet Protocol</a>
+ */
+public final class IpSecTransform implements AutoCloseable {
+    private static final String TAG = "IpSecTransform";
+
+    /** @hide */
+    public static final int MODE_TRANSPORT = 0;
+
+    /** @hide */
+    public static final int MODE_TUNNEL = 1;
+
+    /** @hide */
+    public static final int ENCAP_NONE = 0;
+
+    /**
+     * IPsec traffic will be encapsulated within UDP, but with 8 zero-value bytes between the UDP
+     * header and payload. This prevents traffic from being interpreted as ESP or IKEv2.
+     *
+     * @hide
+     */
+    public static final int ENCAP_ESPINUDP_NON_IKE = 1;
+
+    /**
+     * IPsec traffic will be encapsulated within UDP as per
+     * <a href="https://tools.ietf.org/html/rfc3948">RFC 3498</a>.
+     *
+     * @hide
+     */
+    public static final int ENCAP_ESPINUDP = 2;
+
+    /** @hide */
+    @IntDef(value = {ENCAP_NONE, ENCAP_ESPINUDP, ENCAP_ESPINUDP_NON_IKE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EncapType {}
+
+    /** @hide */
+    @VisibleForTesting
+    public IpSecTransform(Context context, IpSecConfig config) {
+        mContext = context;
+        mConfig = new IpSecConfig(config);
+        mResourceId = INVALID_RESOURCE_ID;
+    }
+
+    private IpSecManager getIpSecManager(Context context) {
+        return context.getSystemService(IpSecManager.class);
+    }
+    /**
+     * Checks the result status and throws an appropriate exception if the status is not Status.OK.
+     */
+    private void checkResultStatus(int status)
+            throws IOException, IpSecManager.ResourceUnavailableException,
+                    IpSecManager.SpiUnavailableException {
+        switch (status) {
+            case IpSecManager.Status.OK:
+                return;
+                // TODO: Pass Error string back from bundle so that errors can be more specific
+            case IpSecManager.Status.RESOURCE_UNAVAILABLE:
+                throw new IpSecManager.ResourceUnavailableException(
+                        "Failed to allocate a new IpSecTransform");
+            case IpSecManager.Status.SPI_UNAVAILABLE:
+                Log.wtf(TAG, "Attempting to use an SPI that was somehow not reserved");
+                // Fall through
+            default:
+                throw new IllegalStateException(
+                        "Failed to Create a Transform with status code " + status);
+        }
+    }
+
+    private IpSecTransform activate()
+            throws IOException, IpSecManager.ResourceUnavailableException,
+                    IpSecManager.SpiUnavailableException {
+        synchronized (this) {
+            try {
+                IpSecTransformResponse result = getIpSecManager(mContext).createTransform(
+                        mConfig, new Binder(), mContext.getOpPackageName());
+                int status = result.status;
+                checkResultStatus(status);
+                mResourceId = result.resourceId;
+                Log.d(TAG, "Added Transform with Id " + mResourceId);
+                mCloseGuard.open("build");
+            } catch (ServiceSpecificException e) {
+                throw IpSecManager.rethrowUncheckedExceptionFromServiceSpecificException(e);
+            }
+        }
+
+        return this;
+    }
+
+    /**
+     * Standard equals.
+     */
+    public boolean equals(@Nullable Object other) {
+        if (this == other) return true;
+        if (!(other instanceof IpSecTransform)) return false;
+        final IpSecTransform rhs = (IpSecTransform) other;
+        return getConfig().equals(rhs.getConfig()) && mResourceId == rhs.mResourceId;
+    }
+
+    /**
+     * Deactivate this {@code IpSecTransform} and free allocated resources.
+     *
+     * <p>Deactivating a transform while it is still applied to a socket will result in errors on
+     * that socket. Make sure to remove transforms by calling {@link
+     * IpSecManager#removeTransportModeTransforms}. Note, removing an {@code IpSecTransform} from a
+     * socket will not deactivate it (because one transform may be applied to multiple sockets).
+     *
+     * <p>It is safe to call this method on a transform that has already been deactivated.
+     */
+    public void close() {
+        Log.d(TAG, "Removing Transform with Id " + mResourceId);
+
+        // Always safe to attempt cleanup
+        if (mResourceId == INVALID_RESOURCE_ID) {
+            mCloseGuard.close();
+            return;
+        }
+        try {
+            getIpSecManager(mContext).deleteTransform(mResourceId);
+        } catch (Exception e) {
+            // On close we swallow all random exceptions since failure to close is not
+            // actionable by the user.
+            Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+        } finally {
+            mResourceId = INVALID_RESOURCE_ID;
+            mCloseGuard.close();
+        }
+    }
+
+    /** Check that the transform was closed properly. */
+    @Override
+    protected void finalize() throws Throwable {
+        if (mCloseGuard != null) {
+            mCloseGuard.warnIfOpen();
+        }
+        close();
+    }
+
+    /* Package */
+    IpSecConfig getConfig() {
+        return mConfig;
+    }
+
+    private final IpSecConfig mConfig;
+    private int mResourceId;
+    private final Context mContext;
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+
+    /** @hide */
+    @VisibleForTesting
+    public int getResourceId() {
+        return mResourceId;
+    }
+
+    /**
+     * A callback class to provide status information regarding a NAT-T keepalive session
+     *
+     * <p>Use this callback to receive status information regarding a NAT-T keepalive session
+     * by registering it when calling {@link #startNattKeepalive}.
+     *
+     * @hide
+     */
+    public static class NattKeepaliveCallback {
+        /** The specified {@code Network} is not connected. */
+        public static final int ERROR_INVALID_NETWORK = 1;
+        /** The hardware does not support this request. */
+        public static final int ERROR_HARDWARE_UNSUPPORTED = 2;
+        /** The hardware returned an error. */
+        public static final int ERROR_HARDWARE_ERROR = 3;
+
+        /** The requested keepalive was successfully started. */
+        public void onStarted() {}
+        /** The keepalive was successfully stopped. */
+        public void onStopped() {}
+        /** An error occurred. */
+        public void onError(int error) {}
+    }
+
+    /** This class is used to build {@link IpSecTransform} objects. */
+    public static class Builder {
+        private Context mContext;
+        private IpSecConfig mConfig;
+
+        /**
+         * Set the encryption algorithm.
+         *
+         * <p>Encryption is mutually exclusive with authenticated encryption.
+         *
+         * @param algo {@link IpSecAlgorithm} specifying the encryption to be applied.
+         */
+        @NonNull
+        public IpSecTransform.Builder setEncryption(@NonNull IpSecAlgorithm algo) {
+            // TODO: throw IllegalArgumentException if algo is not an encryption algorithm.
+            Objects.requireNonNull(algo);
+            mConfig.setEncryption(algo);
+            return this;
+        }
+
+        /**
+         * Set the authentication (integrity) algorithm.
+         *
+         * <p>Authentication is mutually exclusive with authenticated encryption.
+         *
+         * @param algo {@link IpSecAlgorithm} specifying the authentication to be applied.
+         */
+        @NonNull
+        public IpSecTransform.Builder setAuthentication(@NonNull IpSecAlgorithm algo) {
+            // TODO: throw IllegalArgumentException if algo is not an authentication algorithm.
+            Objects.requireNonNull(algo);
+            mConfig.setAuthentication(algo);
+            return this;
+        }
+
+        /**
+         * Set the authenticated encryption algorithm.
+         *
+         * <p>The Authenticated Encryption (AE) class of algorithms are also known as
+         * Authenticated Encryption with Associated Data (AEAD) algorithms, or Combined mode
+         * algorithms (as referred to in
+         * <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>).
+         *
+         * <p>Authenticated encryption is mutually exclusive with encryption and authentication.
+         *
+         * @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to
+         *     be applied.
+         */
+        @NonNull
+        public IpSecTransform.Builder setAuthenticatedEncryption(@NonNull IpSecAlgorithm algo) {
+            Objects.requireNonNull(algo);
+            mConfig.setAuthenticatedEncryption(algo);
+            return this;
+        }
+
+        /**
+         * Add UDP encapsulation to an IPv4 transform.
+         *
+         * <p>This allows IPsec traffic to pass through a NAT.
+         *
+         * @see <a href="https://tools.ietf.org/html/rfc3948">RFC 3948, UDP Encapsulation of IPsec
+         *     ESP Packets</a>
+         * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.23">RFC 7296 section 2.23,
+         *     NAT Traversal of IKEv2</a>
+         * @param localSocket a socket for sending and receiving encapsulated traffic
+         * @param remotePort the UDP port number of the remote host that will send and receive
+         *     encapsulated traffic. In the case of IKEv2, this should be port 4500.
+         */
+        @NonNull
+        public IpSecTransform.Builder setIpv4Encapsulation(
+                @NonNull IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
+            Objects.requireNonNull(localSocket);
+            mConfig.setEncapType(ENCAP_ESPINUDP);
+            if (localSocket.getResourceId() == INVALID_RESOURCE_ID) {
+                throw new IllegalArgumentException("Invalid UdpEncapsulationSocket");
+            }
+            mConfig.setEncapSocketResourceId(localSocket.getResourceId());
+            mConfig.setEncapRemotePort(remotePort);
+            return this;
+        }
+
+        /**
+         * Build a transport mode {@link IpSecTransform}.
+         *
+         * <p>This builds and activates a transport mode transform. Note that an active transform
+         * will not affect any network traffic until it has been applied to one or more sockets.
+         *
+         * @see IpSecManager#applyTransportModeTransform
+         * @param sourceAddress the source {@code InetAddress} of traffic on sockets that will use
+         *     this transform; this address must belong to the Network used by all sockets that
+         *     utilize this transform; if provided, then only traffic originating from the
+         *     specified source address will be processed.
+         * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
+         *     traffic
+         * @throws IllegalArgumentException indicating that a particular combination of transform
+         *     properties is invalid
+         * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
+         *     are active
+         * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
+         *     collides with an existing transform
+         * @throws IOException indicating other errors
+         */
+        @NonNull
+        public IpSecTransform buildTransportModeTransform(
+                @NonNull InetAddress sourceAddress,
+                @NonNull IpSecManager.SecurityParameterIndex spi)
+                throws IpSecManager.ResourceUnavailableException,
+                        IpSecManager.SpiUnavailableException, IOException {
+            Objects.requireNonNull(sourceAddress);
+            Objects.requireNonNull(spi);
+            if (spi.getResourceId() == INVALID_RESOURCE_ID) {
+                throw new IllegalArgumentException("Invalid SecurityParameterIndex");
+            }
+            mConfig.setMode(MODE_TRANSPORT);
+            mConfig.setSourceAddress(sourceAddress.getHostAddress());
+            mConfig.setSpiResourceId(spi.getResourceId());
+            // FIXME: modifying a builder after calling build can change the built transform.
+            return new IpSecTransform(mContext, mConfig).activate();
+        }
+
+        /**
+         * Build and return an {@link IpSecTransform} object as a Tunnel Mode Transform. Some
+         * parameters have interdependencies that are checked at build time.
+         *
+         * @param sourceAddress the {@link InetAddress} that provides the source address for this
+         *     IPsec tunnel. This is almost certainly an address belonging to the {@link Network}
+         *     that will originate the traffic, which is set as the {@link #setUnderlyingNetwork}.
+         * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
+         *     traffic
+         * @throws IllegalArgumentException indicating that a particular combination of transform
+         *     properties is invalid.
+         * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
+         *     are active
+         * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
+         *     collides with an existing transform
+         * @throws IOException indicating other errors
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+        @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+        public IpSecTransform buildTunnelModeTransform(
+                @NonNull InetAddress sourceAddress,
+                @NonNull IpSecManager.SecurityParameterIndex spi)
+                throws IpSecManager.ResourceUnavailableException,
+                        IpSecManager.SpiUnavailableException, IOException {
+            Objects.requireNonNull(sourceAddress);
+            Objects.requireNonNull(spi);
+            if (spi.getResourceId() == INVALID_RESOURCE_ID) {
+                throw new IllegalArgumentException("Invalid SecurityParameterIndex");
+            }
+            mConfig.setMode(MODE_TUNNEL);
+            mConfig.setSourceAddress(sourceAddress.getHostAddress());
+            mConfig.setSpiResourceId(spi.getResourceId());
+            return new IpSecTransform(mContext, mConfig).activate();
+        }
+
+        /**
+         * Create a new IpSecTransform.Builder.
+         *
+         * @param context current context
+         */
+        public Builder(@NonNull Context context) {
+            Objects.requireNonNull(context);
+            mContext = context;
+            mConfig = new IpSecConfig();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+            .append("IpSecTransform{resourceId=")
+            .append(mResourceId)
+            .append("}")
+            .toString();
+    }
+}
diff --git a/framework-t/src/android/net/IpSecTransformResponse.aidl b/framework-t/src/android/net/IpSecTransformResponse.aidl
new file mode 100644
index 0000000..546230d
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformResponse.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/** @hide */
+parcelable IpSecTransformResponse;
diff --git a/framework-t/src/android/net/IpSecTransformResponse.java b/framework-t/src/android/net/IpSecTransformResponse.java
new file mode 100644
index 0000000..363f316
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformResponse.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an IpSecTransform resource Id and and corresponding status from the
+ * IpSecService to an IpSecTransform object.
+ *
+ * @hide
+ */
+public final class IpSecTransformResponse implements Parcelable {
+    private static final String TAG = "IpSecTransformResponse";
+
+    public final int resourceId;
+    public final int status;
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+    }
+
+    public IpSecTransformResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+    }
+
+    public IpSecTransformResponse(int inStatus, int inResourceId) {
+        status = inStatus;
+        resourceId = inResourceId;
+    }
+
+    private IpSecTransformResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+    }
+
+    @android.annotation.NonNull
+    public static final Parcelable.Creator<IpSecTransformResponse> CREATOR =
+            new Parcelable.Creator<IpSecTransformResponse>() {
+                public IpSecTransformResponse createFromParcel(Parcel in) {
+                    return new IpSecTransformResponse(in);
+                }
+
+                public IpSecTransformResponse[] newArray(int size) {
+                    return new IpSecTransformResponse[size];
+                }
+            };
+}
diff --git a/framework-t/src/android/net/IpSecTunnelInterfaceResponse.aidl b/framework-t/src/android/net/IpSecTunnelInterfaceResponse.aidl
new file mode 100644
index 0000000..7239221
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTunnelInterfaceResponse.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/** @hide */
+parcelable IpSecTunnelInterfaceResponse;
diff --git a/framework-t/src/android/net/IpSecTunnelInterfaceResponse.java b/framework-t/src/android/net/IpSecTunnelInterfaceResponse.java
new file mode 100644
index 0000000..127e30a
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTunnelInterfaceResponse.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an IpSecTunnelInterface resource Id and and corresponding status
+ * from the IpSecService to an IpSecTunnelInterface object.
+ *
+ * @hide
+ */
+public final class IpSecTunnelInterfaceResponse implements Parcelable {
+    private static final String TAG = "IpSecTunnelInterfaceResponse";
+
+    public final int resourceId;
+    public final String interfaceName;
+    public final int status;
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+        out.writeString(interfaceName);
+    }
+
+    public IpSecTunnelInterfaceResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+        interfaceName = "";
+    }
+
+    public IpSecTunnelInterfaceResponse(int inStatus, int inResourceId, String inInterfaceName) {
+        status = inStatus;
+        resourceId = inResourceId;
+        interfaceName = inInterfaceName;
+    }
+
+    private IpSecTunnelInterfaceResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+        interfaceName = in.readString();
+    }
+
+    @android.annotation.NonNull
+    public static final Parcelable.Creator<IpSecTunnelInterfaceResponse> CREATOR =
+            new Parcelable.Creator<IpSecTunnelInterfaceResponse>() {
+                public IpSecTunnelInterfaceResponse createFromParcel(Parcel in) {
+                    return new IpSecTunnelInterfaceResponse(in);
+                }
+
+                public IpSecTunnelInterfaceResponse[] newArray(int size) {
+                    return new IpSecTunnelInterfaceResponse[size];
+                }
+            };
+}
diff --git a/framework-t/src/android/net/IpSecUdpEncapResponse.aidl b/framework-t/src/android/net/IpSecUdpEncapResponse.aidl
new file mode 100644
index 0000000..5e451f3
--- /dev/null
+++ b/framework-t/src/android/net/IpSecUdpEncapResponse.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/** @hide */
+parcelable IpSecUdpEncapResponse;
diff --git a/framework-t/src/android/net/IpSecUdpEncapResponse.java b/framework-t/src/android/net/IpSecUdpEncapResponse.java
new file mode 100644
index 0000000..732cf19
--- /dev/null
+++ b/framework-t/src/android/net/IpSecUdpEncapResponse.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * This class is used to return a UDP Socket and corresponding status from the IpSecService to an
+ * IpSecManager.UdpEncapsulationSocket.
+ *
+ * @hide
+ */
+public final class IpSecUdpEncapResponse implements Parcelable {
+    private static final String TAG = "IpSecUdpEncapResponse";
+
+    public final int resourceId;
+    public final int port;
+    public final int status;
+    // There is a weird asymmetry with FileDescriptor: you can write a FileDescriptor
+    // but you read a ParcelFileDescriptor. To circumvent this, when we receive a FD
+    // from the user, we immediately create a ParcelFileDescriptor DUP, which we invalidate
+    // on writeParcel() by setting the flag to do close-on-write.
+    // TODO: tests to ensure this doesn't leak
+    public final ParcelFileDescriptor fileDescriptor;
+
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return (fileDescriptor != null) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+        out.writeInt(port);
+        out.writeParcelable(fileDescriptor, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+    }
+
+    public IpSecUdpEncapResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+        port = -1;
+        fileDescriptor = null; // yes I know it's redundant, but readability
+    }
+
+    public IpSecUdpEncapResponse(int inStatus, int inResourceId, int inPort, FileDescriptor inFd)
+            throws IOException {
+        if (inStatus == IpSecManager.Status.OK && inFd == null) {
+            throw new IllegalArgumentException("Valid status implies FD must be non-null");
+        }
+        status = inStatus;
+        resourceId = inResourceId;
+        port = inPort;
+        fileDescriptor = (status == IpSecManager.Status.OK) ? ParcelFileDescriptor.dup(inFd) : null;
+    }
+
+    private IpSecUdpEncapResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+        port = in.readInt();
+        fileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
+    }
+
+    @android.annotation.NonNull
+    public static final Parcelable.Creator<IpSecUdpEncapResponse> CREATOR =
+            new Parcelable.Creator<IpSecUdpEncapResponse>() {
+                public IpSecUdpEncapResponse createFromParcel(Parcel in) {
+                    return new IpSecUdpEncapResponse(in);
+                }
+
+                public IpSecUdpEncapResponse[] newArray(int size) {
+                    return new IpSecUdpEncapResponse[size];
+                }
+            };
+}
diff --git a/framework-t/src/android/net/NetworkIdentity.java b/framework-t/src/android/net/NetworkIdentity.java
new file mode 100644
index 0000000..da5f88d
--- /dev/null
+++ b/framework-t/src/android/net/NetworkIdentity.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.NetworkTemplate.NETWORK_TYPE_ALL;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
+import android.net.wifi.WifiInfo;
+import android.service.NetworkIdentityProto;
+import android.telephony.TelephonyManager;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.NetworkCapabilitiesUtils;
+import com.android.net.module.util.NetworkIdentityUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * Network definition that includes strong identity. Analogous to combining
+ * {@link NetworkCapabilities} and an IMSI.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public class NetworkIdentity {
+    private static final String TAG = "NetworkIdentity";
+
+    /** @hide */
+    // TODO: Remove this after migrating all callers to use
+    //  {@link NetworkTemplate#NETWORK_TYPE_ALL} instead.
+    public static final int SUBTYPE_COMBINED = -1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "OEM_MANAGED_" }, flag = true, value = {
+            NetworkTemplate.OEM_MANAGED_NO,
+            NetworkTemplate.OEM_MANAGED_PAID,
+            NetworkTemplate.OEM_MANAGED_PRIVATE
+    })
+    public @interface OemManaged{}
+
+    /**
+     * Network has no {@code NetworkCapabilities#NET_CAPABILITY_OEM_*}.
+     * @hide
+     */
+    public static final int OEM_NONE = 0x0;
+    /**
+     * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PAID}.
+     * @hide
+     */
+    public static final int OEM_PAID = 1 << 0;
+    /**
+     * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PRIVATE}.
+     * @hide
+     */
+    public static final int OEM_PRIVATE = 1 << 1;
+
+    private static final long SUPPORTED_OEM_MANAGED_TYPES = OEM_PAID | OEM_PRIVATE;
+
+    final int mType;
+    final int mRatType;
+    final int mSubId;
+    final String mSubscriberId;
+    final String mWifiNetworkKey;
+    final boolean mRoaming;
+    final boolean mMetered;
+    final boolean mDefaultNetwork;
+    final int mOemManaged;
+
+    /** @hide */
+    public NetworkIdentity(
+            int type, int ratType, @Nullable String subscriberId, @Nullable String wifiNetworkKey,
+            boolean roaming, boolean metered, boolean defaultNetwork, int oemManaged, int subId) {
+        mType = type;
+        mRatType = ratType;
+        mSubscriberId = subscriberId;
+        mWifiNetworkKey = wifiNetworkKey;
+        mRoaming = roaming;
+        mMetered = metered;
+        mDefaultNetwork = defaultNetwork;
+        mOemManaged = oemManaged;
+        mSubId = subId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mRatType, mSubscriberId, mWifiNetworkKey, mRoaming, mMetered,
+                mDefaultNetwork, mOemManaged, mSubId);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof NetworkIdentity) {
+            final NetworkIdentity ident = (NetworkIdentity) obj;
+            return mType == ident.mType && mRatType == ident.mRatType && mRoaming == ident.mRoaming
+                    && Objects.equals(mSubscriberId, ident.mSubscriberId)
+                    && Objects.equals(mWifiNetworkKey, ident.mWifiNetworkKey)
+                    && mMetered == ident.mMetered
+                    && mDefaultNetwork == ident.mDefaultNetwork
+                    && mOemManaged == ident.mOemManaged
+                    && mSubId == ident.mSubId;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder("{");
+        builder.append("type=").append(mType);
+        builder.append(", ratType=");
+        if (mRatType == NETWORK_TYPE_ALL) {
+            builder.append("COMBINED");
+        } else {
+            builder.append(mRatType);
+        }
+        if (mSubscriberId != null) {
+            builder.append(", subscriberId=")
+                    .append(NetworkIdentityUtils.scrubSubscriberId(mSubscriberId));
+        }
+        if (mWifiNetworkKey != null) {
+            builder.append(", wifiNetworkKey=").append(mWifiNetworkKey);
+        }
+        if (mRoaming) {
+            builder.append(", ROAMING");
+        }
+        builder.append(", metered=").append(mMetered);
+        builder.append(", defaultNetwork=").append(mDefaultNetwork);
+        builder.append(", oemManaged=").append(getOemManagedNames(mOemManaged));
+        builder.append(", subId=").append(mSubId);
+        return builder.append("}").toString();
+    }
+
+    /**
+     * Get the human readable representation of a bitfield representing the OEM managed state of a
+     * network.
+     */
+    static String getOemManagedNames(int oemManaged) {
+        if (oemManaged == OEM_NONE) {
+            return "OEM_NONE";
+        }
+        final int[] bitPositions = NetworkCapabilitiesUtils.unpackBits(oemManaged);
+        final ArrayList<String> oemManagedNames = new ArrayList<String>();
+        for (int position : bitPositions) {
+            oemManagedNames.add(nameOfOemManaged(1 << position));
+        }
+        return String.join(",", oemManagedNames);
+    }
+
+    private static String nameOfOemManaged(int oemManagedBit) {
+        switch (oemManagedBit) {
+            case OEM_PAID:
+                return "OEM_PAID";
+            case OEM_PRIVATE:
+                return "OEM_PRIVATE";
+            default:
+                return "Invalid(" + oemManagedBit + ")";
+        }
+    }
+
+    /** @hide */
+    public void dumpDebug(ProtoOutputStream proto, long tag) {
+        final long start = proto.start(tag);
+
+        proto.write(NetworkIdentityProto.TYPE, mType);
+
+        // TODO: dump mRatType as well.
+
+        proto.write(NetworkIdentityProto.ROAMING, mRoaming);
+        proto.write(NetworkIdentityProto.METERED, mMetered);
+        proto.write(NetworkIdentityProto.DEFAULT_NETWORK, mDefaultNetwork);
+        proto.write(NetworkIdentityProto.OEM_MANAGED_NETWORK, mOemManaged);
+
+        proto.end(start);
+    }
+
+    /** Get the network type of this instance. */
+    public int getType() {
+        return mType;
+    }
+
+    /** Get the Radio Access Technology(RAT) type of this instance. */
+    public int getRatType() {
+        return mRatType;
+    }
+
+    /** Get the Subscriber Id of this instance. */
+    @Nullable
+    public String getSubscriberId() {
+        return mSubscriberId;
+    }
+
+    /** Get the Wifi Network Key of this instance. See {@link WifiInfo#getNetworkKey()}. */
+    @Nullable
+    public String getWifiNetworkKey() {
+        return mWifiNetworkKey;
+    }
+
+    /** @hide */
+    // TODO: Remove this function after all callers are removed.
+    public boolean getRoaming() {
+        return mRoaming;
+    }
+
+    /** Return whether this network is roaming. */
+    public boolean isRoaming() {
+        return mRoaming;
+    }
+
+    /** @hide */
+    // TODO: Remove this function after all callers are removed.
+    public boolean getMetered() {
+        return mMetered;
+    }
+
+    /** Return whether this network is metered. */
+    public boolean isMetered() {
+        return mMetered;
+    }
+
+    /** @hide */
+    // TODO: Remove this function after all callers are removed.
+    public boolean getDefaultNetwork() {
+        return mDefaultNetwork;
+    }
+
+    /** Return whether this network is the default network. */
+    public boolean isDefaultNetwork() {
+        return mDefaultNetwork;
+    }
+
+    /** Get the OEM managed type of this instance. */
+    public int getOemManaged() {
+        return mOemManaged;
+    }
+
+    /** Get the SubId of this instance. */
+    public int getSubId() {
+        return mSubId;
+    }
+
+    /**
+     * Assemble a {@link NetworkIdentity} from the passed arguments.
+     *
+     * This methods builds an identity based on the capabilities of the network in the
+     * snapshot and other passed arguments. The identity is used as a key to record data usage.
+     *
+     * @param snapshot the snapshot of network state. See {@link NetworkStateSnapshot}.
+     * @param defaultNetwork whether the network is a default network.
+     * @param ratType the Radio Access Technology(RAT) type of the network. Or
+     *                {@link TelephonyManager#NETWORK_TYPE_UNKNOWN} if not applicable.
+     *                See {@code TelephonyManager.NETWORK_TYPE_*}.
+     * @hide
+     * @deprecated See {@link NetworkIdentity.Builder}.
+     */
+    // TODO: Remove this after all callers are migrated to use new Api.
+    @Deprecated
+    @NonNull
+    public static NetworkIdentity buildNetworkIdentity(Context context,
+            @NonNull NetworkStateSnapshot snapshot, boolean defaultNetwork, int ratType) {
+        final NetworkIdentity.Builder builder = new NetworkIdentity.Builder()
+                .setNetworkStateSnapshot(snapshot).setDefaultNetwork(defaultNetwork)
+                .setSubId(snapshot.getSubId());
+        if (snapshot.getLegacyType() == TYPE_MOBILE && ratType != NETWORK_TYPE_ALL) {
+            builder.setRatType(ratType);
+        }
+        return builder.build();
+    }
+
+    /**
+     * Builds a bitfield of {@code NetworkIdentity.OEM_*} based on {@link NetworkCapabilities}.
+     * @hide
+     */
+    public static int getOemBitfield(@NonNull NetworkCapabilities nc) {
+        int oemManaged = OEM_NONE;
+
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID)) {
+            oemManaged |= OEM_PAID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)) {
+            oemManaged |= OEM_PRIVATE;
+        }
+
+        return oemManaged;
+    }
+
+    /** @hide */
+    public static int compare(@NonNull NetworkIdentity left, @NonNull NetworkIdentity right) {
+        Objects.requireNonNull(right);
+        int res = Integer.compare(left.mType, right.mType);
+        if (res == 0) {
+            res = Integer.compare(left.mRatType, right.mRatType);
+        }
+        if (res == 0 && left.mSubscriberId != null && right.mSubscriberId != null) {
+            res = left.mSubscriberId.compareTo(right.mSubscriberId);
+        }
+        if (res == 0 && left.mWifiNetworkKey != null && right.mWifiNetworkKey != null) {
+            res = left.mWifiNetworkKey.compareTo(right.mWifiNetworkKey);
+        }
+        if (res == 0) {
+            res = Boolean.compare(left.mRoaming, right.mRoaming);
+        }
+        if (res == 0) {
+            res = Boolean.compare(left.mMetered, right.mMetered);
+        }
+        if (res == 0) {
+            res = Boolean.compare(left.mDefaultNetwork, right.mDefaultNetwork);
+        }
+        if (res == 0) {
+            res = Integer.compare(left.mOemManaged, right.mOemManaged);
+        }
+        if (res == 0) {
+            res = Integer.compare(left.mSubId, right.mSubId);
+        }
+        return res;
+    }
+
+    /**
+     * Builder class for {@link NetworkIdentity}.
+     */
+    public static final class Builder {
+        // Need to be synchronized with ConnectivityManager.
+        // TODO: Use {@link ConnectivityManager#MAX_NETWORK_TYPE} when this file is in the module.
+        private static final int MAX_NETWORK_TYPE = 18; // TYPE_TEST
+        private static final int MIN_NETWORK_TYPE = TYPE_MOBILE;
+
+        private int mType;
+        private int mRatType;
+        private String mSubscriberId;
+        private String mWifiNetworkKey;
+        private boolean mRoaming;
+        private boolean mMetered;
+        private boolean mDefaultNetwork;
+        private int mOemManaged;
+        private int mSubId;
+
+        /**
+         * Creates a new Builder.
+         */
+        public Builder() {
+            // Initialize with default values. Will be overwritten by setters.
+            mType = ConnectivityManager.TYPE_NONE;
+            mRatType = NetworkTemplate.NETWORK_TYPE_ALL;
+            mSubscriberId = null;
+            mWifiNetworkKey = null;
+            mRoaming = false;
+            mMetered = false;
+            mDefaultNetwork = false;
+            mOemManaged = NetworkTemplate.OEM_MANAGED_NO;
+            mSubId = INVALID_SUBSCRIPTION_ID;
+        }
+
+        /**
+         * Add an {@link NetworkStateSnapshot} into the {@link NetworkIdentity} instance.
+         * This is a useful shorthand that will read from the snapshot and set the
+         * following fields, if they are set in the snapshot :
+         *  - type
+         *  - subscriberId
+         *  - roaming
+         *  - metered
+         *  - oemManaged
+         *  - wifiNetworkKey
+         *
+         * @param snapshot The target {@link NetworkStateSnapshot} object.
+         * @return The builder object.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setNetworkStateSnapshot(@NonNull NetworkStateSnapshot snapshot) {
+            setType(snapshot.getLegacyType());
+
+            setSubscriberId(snapshot.getSubscriberId());
+            setRoaming(!snapshot.getNetworkCapabilities().hasCapability(
+                    NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING));
+            setMetered(!(snapshot.getNetworkCapabilities().hasCapability(
+                    NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+                    || snapshot.getNetworkCapabilities().hasCapability(
+                    NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)));
+
+            setOemManaged(getOemBitfield(snapshot.getNetworkCapabilities()));
+
+            if (mType == TYPE_WIFI) {
+                final TransportInfo transportInfo = snapshot.getNetworkCapabilities()
+                        .getTransportInfo();
+                if (transportInfo instanceof WifiInfo) {
+                    final WifiInfo info = (WifiInfo) transportInfo;
+                    setWifiNetworkKey(info.getNetworkKey());
+                }
+            }
+            return this;
+        }
+
+        /**
+         * Set the network type of the network.
+         *
+         * @param type the network type. See {@link ConnectivityManager#TYPE_*}.
+         *
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setType(int type) {
+            // Include TYPE_NONE for compatibility, type field might not be filled by some
+            // networks such as test networks.
+            if ((type < MIN_NETWORK_TYPE || MAX_NETWORK_TYPE < type)
+                    && type != ConnectivityManager.TYPE_NONE) {
+                throw new IllegalArgumentException("Invalid network type: " + type);
+            }
+            mType = type;
+            return this;
+        }
+
+        /**
+         * Set the Radio Access Technology(RAT) type of the network.
+         *
+         * No RAT type is specified by default. Call clearRatType to reset.
+         *
+         * @param ratType the Radio Access Technology(RAT) type if applicable. See
+         *                {@code TelephonyManager.NETWORK_TYPE_*}.
+         *
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setRatType(int ratType) {
+            if (!CollectionUtils.contains(TelephonyManager.getAllNetworkTypes(), ratType)
+                    && ratType != TelephonyManager.NETWORK_TYPE_UNKNOWN
+                    && ratType != NetworkStatsManager.NETWORK_TYPE_5G_NSA) {
+                throw new IllegalArgumentException("Invalid ratType " + ratType);
+            }
+            mRatType = ratType;
+            return this;
+        }
+
+        /**
+         * Clear the Radio Access Technology(RAT) type of the network.
+         *
+         * @return this builder.
+         */
+        @NonNull
+        public Builder clearRatType() {
+            mRatType = NetworkTemplate.NETWORK_TYPE_ALL;
+            return this;
+        }
+
+        /**
+         * Set the Subscriber Id.
+         *
+         * @param subscriberId the Subscriber Id of the network. Or null if not applicable.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setSubscriberId(@Nullable String subscriberId) {
+            mSubscriberId = subscriberId;
+            return this;
+        }
+
+        /**
+         * Set the Wifi Network Key.
+         *
+         * @param wifiNetworkKey Wifi Network Key of the network,
+         *                        see {@link WifiInfo#getNetworkKey()}.
+         *                        Or null if not applicable.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setWifiNetworkKey(@Nullable String wifiNetworkKey) {
+            mWifiNetworkKey = wifiNetworkKey;
+            return this;
+        }
+
+        /**
+         * Set whether this network is roaming.
+         *
+         * This field is false by default. Call with false to reset.
+         *
+         * @param roaming the roaming status of the network.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setRoaming(boolean roaming) {
+            mRoaming = roaming;
+            return this;
+        }
+
+        /**
+         * Set whether this network is metered.
+         *
+         * This field is false by default. Call with false to reset.
+         *
+         * @param metered the meteredness of the network.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setMetered(boolean metered) {
+            mMetered = metered;
+            return this;
+        }
+
+        /**
+         * Set whether this network is the default network.
+         *
+         * This field is false by default. Call with false to reset.
+         *
+         * @param defaultNetwork the default network status of the network.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setDefaultNetwork(boolean defaultNetwork) {
+            mDefaultNetwork = defaultNetwork;
+            return this;
+        }
+
+        /**
+         * Set the OEM managed type.
+         *
+         * @param oemManaged Type of OEM managed network or unmanaged networks.
+         *                   See {@code NetworkTemplate#OEM_MANAGED_*}.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setOemManaged(@OemManaged int oemManaged) {
+            // Assert input does not contain illegal oemManage bits.
+            if ((~SUPPORTED_OEM_MANAGED_TYPES & oemManaged) != 0) {
+                throw new IllegalArgumentException("Invalid value for OemManaged : " + oemManaged);
+            }
+            mOemManaged = oemManaged;
+            return this;
+        }
+
+        /**
+         * Set the Subscription Id.
+         *
+         * @param subId the Subscription Id of the network. Or INVALID_SUBSCRIPTION_ID if not
+         *              applicable.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setSubId(int subId) {
+            mSubId = subId;
+            return this;
+        }
+
+        private void ensureValidParameters() {
+            // Assert non-mobile network cannot have a ratType.
+            if (mType != TYPE_MOBILE && mRatType != NetworkTemplate.NETWORK_TYPE_ALL) {
+                throw new IllegalArgumentException(
+                        "Invalid ratType " + mRatType + " for type " + mType);
+            }
+
+            // Assert non-wifi network cannot have a wifi network key.
+            if (mType != TYPE_WIFI && mWifiNetworkKey != null) {
+                throw new IllegalArgumentException("Invalid wifi network key for type " + mType);
+            }
+        }
+
+        /**
+         * Builds the instance of the {@link NetworkIdentity}.
+         *
+         * @return the built instance of {@link NetworkIdentity}.
+         */
+        @NonNull
+        public NetworkIdentity build() {
+            ensureValidParameters();
+            return new NetworkIdentity(mType, mRatType, mSubscriberId, mWifiNetworkKey,
+                    mRoaming, mMetered, mDefaultNetwork, mOemManaged, mSubId);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/NetworkIdentitySet.java b/framework-t/src/android/net/NetworkIdentitySet.java
new file mode 100644
index 0000000..d88408e
--- /dev/null
+++ b/framework-t/src/android/net/NetworkIdentitySet.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.annotation.NonNull;
+import android.service.NetworkIdentitySetProto;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Identity of a {@code iface}, defined by the set of {@link NetworkIdentity}
+ * active on that interface.
+ *
+ * @hide
+ */
+public class NetworkIdentitySet extends HashSet<NetworkIdentity> {
+    private static final int VERSION_INIT = 1;
+    private static final int VERSION_ADD_ROAMING = 2;
+    private static final int VERSION_ADD_NETWORK_ID = 3;
+    private static final int VERSION_ADD_METERED = 4;
+    private static final int VERSION_ADD_DEFAULT_NETWORK = 5;
+    private static final int VERSION_ADD_OEM_MANAGED_NETWORK = 6;
+    private static final int VERSION_ADD_SUB_ID = 7;
+
+    /**
+     * Construct a {@link NetworkIdentitySet} object.
+     */
+    public NetworkIdentitySet() {
+        super();
+    }
+
+    /** @hide */
+    public NetworkIdentitySet(@NonNull Set<NetworkIdentity> ident) {
+        super(ident);
+    }
+
+    /** @hide */
+    public NetworkIdentitySet(DataInput in) throws IOException {
+        final int version = in.readInt();
+        final int size = in.readInt();
+        for (int i = 0; i < size; i++) {
+            if (version <= VERSION_INIT) {
+                final int ignored = in.readInt();
+            }
+            final int type = in.readInt();
+            final int ratType = in.readInt();
+            final String subscriberId = readOptionalString(in);
+            final String networkId;
+            if (version >= VERSION_ADD_NETWORK_ID) {
+                networkId = readOptionalString(in);
+            } else {
+                networkId = null;
+            }
+            final boolean roaming;
+            if (version >= VERSION_ADD_ROAMING) {
+                roaming = in.readBoolean();
+            } else {
+                roaming = false;
+            }
+
+            final boolean metered;
+            if (version >= VERSION_ADD_METERED) {
+                metered = in.readBoolean();
+            } else {
+                // If this is the old data and the type is mobile, treat it as metered. (Note that
+                // if this is a mobile network, TYPE_MOBILE is the only possible type that could be
+                // used.)
+                metered = (type == TYPE_MOBILE);
+            }
+
+            final boolean defaultNetwork;
+            if (version >= VERSION_ADD_DEFAULT_NETWORK) {
+                defaultNetwork = in.readBoolean();
+            } else {
+                defaultNetwork = true;
+            }
+
+            final int oemNetCapabilities;
+            if (version >= VERSION_ADD_OEM_MANAGED_NETWORK) {
+                oemNetCapabilities = in.readInt();
+            } else {
+                oemNetCapabilities = NetworkIdentity.OEM_NONE;
+            }
+
+            final int subId;
+            if (version >= VERSION_ADD_SUB_ID) {
+                subId = in.readInt();
+            } else {
+                subId = INVALID_SUBSCRIPTION_ID;
+            }
+
+            add(new NetworkIdentity(type, ratType, subscriberId, networkId, roaming, metered,
+                    defaultNetwork, oemNetCapabilities, subId));
+        }
+    }
+
+    /**
+     * Method to serialize this object into a {@code DataOutput}.
+     * @hide
+     */
+    public void writeToStream(DataOutput out) throws IOException {
+        out.writeInt(VERSION_ADD_SUB_ID);
+        out.writeInt(size());
+        for (NetworkIdentity ident : this) {
+            out.writeInt(ident.getType());
+            out.writeInt(ident.getRatType());
+            writeOptionalString(out, ident.getSubscriberId());
+            writeOptionalString(out, ident.getWifiNetworkKey());
+            out.writeBoolean(ident.isRoaming());
+            out.writeBoolean(ident.isMetered());
+            out.writeBoolean(ident.isDefaultNetwork());
+            out.writeInt(ident.getOemManaged());
+            out.writeInt(ident.getSubId());
+        }
+    }
+
+    /**
+     * @return whether any {@link NetworkIdentity} in this set is considered metered.
+     * @hide
+     */
+    public boolean isAnyMemberMetered() {
+        if (isEmpty()) {
+            return false;
+        }
+        for (NetworkIdentity ident : this) {
+            if (ident.isMetered()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return whether any {@link NetworkIdentity} in this set is considered roaming.
+     * @hide
+     */
+    public boolean isAnyMemberRoaming() {
+        if (isEmpty()) {
+            return false;
+        }
+        for (NetworkIdentity ident : this) {
+            if (ident.isRoaming()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return whether any {@link NetworkIdentity} in this set is considered on the default
+     *         network.
+     * @hide
+     */
+    public boolean areAllMembersOnDefaultNetwork() {
+        if (isEmpty()) {
+            return true;
+        }
+        for (NetworkIdentity ident : this) {
+            if (!ident.isDefaultNetwork()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static void writeOptionalString(DataOutput out, String value) throws IOException {
+        if (value != null) {
+            out.writeByte(1);
+            out.writeUTF(value);
+        } else {
+            out.writeByte(0);
+        }
+    }
+
+    private static String readOptionalString(DataInput in) throws IOException {
+        if (in.readByte() != 0) {
+            return in.readUTF();
+        } else {
+            return null;
+        }
+    }
+
+    public static int compare(@NonNull NetworkIdentitySet left, @NonNull NetworkIdentitySet right) {
+        Objects.requireNonNull(left);
+        Objects.requireNonNull(right);
+        if (left.isEmpty() && right.isEmpty()) return 0;
+        if (left.isEmpty()) return -1;
+        if (right.isEmpty()) return 1;
+
+        final NetworkIdentity leftIdent = left.iterator().next();
+        final NetworkIdentity rightIdent = right.iterator().next();
+        return NetworkIdentity.compare(leftIdent, rightIdent);
+    }
+
+    /**
+     * Method to dump this object into proto debug file.
+     * @hide
+     */
+    public void dumpDebug(ProtoOutputStream proto, long tag) {
+        final long start = proto.start(tag);
+
+        for (NetworkIdentity ident : this) {
+            ident.dumpDebug(proto, NetworkIdentitySetProto.IDENTITIES);
+        }
+
+        proto.end(start);
+    }
+}
diff --git a/framework-t/src/android/net/NetworkStateSnapshot.java b/framework-t/src/android/net/NetworkStateSnapshot.java
new file mode 100644
index 0000000..d3f785a
--- /dev/null
+++ b/framework-t/src/android/net/NetworkStateSnapshot.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.module.util.NetworkIdentityUtils;
+
+import java.util.Objects;
+
+/**
+ * Snapshot of network state.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class NetworkStateSnapshot implements Parcelable {
+    /** The network associated with this snapshot. */
+    @NonNull
+    private final Network mNetwork;
+
+    /** The {@link NetworkCapabilities} of the network associated with this snapshot. */
+    @NonNull
+    private final NetworkCapabilities mNetworkCapabilities;
+
+    /** The {@link LinkProperties} of the network associated with this snapshot. */
+    @NonNull
+    private final LinkProperties mLinkProperties;
+
+    /**
+     * The Subscriber Id of the network associated with this snapshot. See
+     * {@link android.telephony.TelephonyManager#getSubscriberId()}.
+     */
+    @Nullable
+    private final String mSubscriberId;
+
+    /**
+     * The legacy type of the network associated with this snapshot. See
+     * {@code ConnectivityManager#TYPE_*}.
+     */
+    private final int mLegacyType;
+
+    public NetworkStateSnapshot(@NonNull Network network,
+            @NonNull NetworkCapabilities networkCapabilities,
+            @NonNull LinkProperties linkProperties,
+            @Nullable String subscriberId, int legacyType) {
+        mNetwork = Objects.requireNonNull(network);
+        mNetworkCapabilities = Objects.requireNonNull(networkCapabilities);
+        mLinkProperties = Objects.requireNonNull(linkProperties);
+        mSubscriberId = subscriberId;
+        mLegacyType = legacyType;
+    }
+
+    /** @hide */
+    public NetworkStateSnapshot(@NonNull Parcel in) {
+        mNetwork = in.readParcelable(null);
+        mNetworkCapabilities = in.readParcelable(null);
+        mLinkProperties = in.readParcelable(null);
+        mSubscriberId = in.readString();
+        mLegacyType = in.readInt();
+    }
+
+    /** Get the network associated with this snapshot */
+    @NonNull
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    /** Get {@link NetworkCapabilities} of the network associated with this snapshot. */
+    @NonNull
+    public NetworkCapabilities getNetworkCapabilities() {
+        return mNetworkCapabilities;
+    }
+
+    /** Get the {@link LinkProperties} of the network associated with this snapshot. */
+    @NonNull
+    public LinkProperties getLinkProperties() {
+        return mLinkProperties;
+    }
+
+    /**
+     * Get the Subscriber Id of the network associated with this snapshot.
+     * @deprecated Please use #getSubId, which doesn't return personally identifiable
+     * information.
+     */
+    @Deprecated
+    @Nullable
+    public String getSubscriberId() {
+        return mSubscriberId;
+    }
+
+    /** Get the subId of the network associated with this snapshot. */
+    public int getSubId() {
+        if (mNetworkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+            final NetworkSpecifier spec = mNetworkCapabilities.getNetworkSpecifier();
+            if (spec instanceof TelephonyNetworkSpecifier) {
+                return ((TelephonyNetworkSpecifier) spec).getSubscriptionId();
+            }
+        }
+        return INVALID_SUBSCRIPTION_ID;
+    }
+
+
+    /**
+     * Get the legacy type of the network associated with this snapshot.
+     * @return the legacy network type. See {@code ConnectivityManager#TYPE_*}.
+     */
+    public int getLegacyType() {
+        return mLegacyType;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeParcelable(mNetwork, flags);
+        out.writeParcelable(mNetworkCapabilities, flags);
+        out.writeParcelable(mLinkProperties, flags);
+        out.writeString(mSubscriberId);
+        out.writeInt(mLegacyType);
+    }
+
+    @NonNull
+    public static final Creator<NetworkStateSnapshot> CREATOR =
+            new Creator<NetworkStateSnapshot>() {
+        @NonNull
+        @Override
+        public NetworkStateSnapshot createFromParcel(@NonNull Parcel in) {
+            return new NetworkStateSnapshot(in);
+        }
+
+        @NonNull
+        @Override
+        public NetworkStateSnapshot[] newArray(int size) {
+            return new NetworkStateSnapshot[size];
+        }
+    };
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof NetworkStateSnapshot)) return false;
+        NetworkStateSnapshot that = (NetworkStateSnapshot) o;
+        return mLegacyType == that.mLegacyType
+                && Objects.equals(mNetwork, that.mNetwork)
+                && Objects.equals(mNetworkCapabilities, that.mNetworkCapabilities)
+                && Objects.equals(mLinkProperties, that.mLinkProperties)
+                && Objects.equals(mSubscriberId, that.mSubscriberId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mNetwork,
+                mNetworkCapabilities, mLinkProperties, mSubscriberId, mLegacyType);
+    }
+
+    @Override
+    public String toString() {
+        return "NetworkStateSnapshot{"
+                + "network=" + mNetwork
+                + ", networkCapabilities=" + mNetworkCapabilities
+                + ", linkProperties=" + mLinkProperties
+                + ", subscriberId='" + NetworkIdentityUtils.scrubSubscriberId(mSubscriberId) + '\''
+                + ", legacyType=" + mLegacyType
+                + '}';
+    }
+}
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
new file mode 100644
index 0000000..51ff5ec
--- /dev/null
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -0,0 +1,1836 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
+
+import libcore.util.EmptyArray;
+
+import java.io.CharArrayWriter;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * Collection of active network statistics. Can contain summary details across
+ * all interfaces, or details with per-UID granularity. Internally stores data
+ * as a large table, closely matching {@code /proc/} data format. This structure
+ * optimizes for rapid in-memory comparison, but consider using
+ * {@link NetworkStatsHistory} when persisting.
+ *
+ * @hide
+ */
+// @NotThreadSafe
+@SystemApi
+public final class NetworkStats implements Parcelable, Iterable<NetworkStats.Entry> {
+    private static final String TAG = "NetworkStats";
+
+    /**
+     * {@link #iface} value when interface details unavailable.
+     * @hide
+     */
+    @Nullable public static final String IFACE_ALL = null;
+
+    /**
+     * Virtual network interface for video telephony. This is for VT data usage counting
+     * purpose.
+     */
+    public static final String IFACE_VT = "vt_data0";
+
+    /** {@link #uid} value when UID details unavailable. */
+    public static final int UID_ALL = -1;
+    /** Special UID value for data usage by tethering. */
+    public static final int UID_TETHERING = -5;
+
+    /**
+     * {@link #tag} value matching any tag.
+     * @hide
+     */
+    // TODO: Rename TAG_ALL to TAG_ANY.
+    public static final int TAG_ALL = -1;
+    /** {@link #set} value for all sets combined, not including debug sets. */
+    public static final int SET_ALL = -1;
+    /** {@link #set} value where background data is accounted. */
+    public static final int SET_DEFAULT = 0;
+    /** {@link #set} value where foreground data is accounted. */
+    public static final int SET_FOREGROUND = 1;
+    /**
+     * All {@link #set} value greater than SET_DEBUG_START are debug {@link #set} values.
+     * @hide
+     */
+    public static final int SET_DEBUG_START = 1000;
+    /**
+     * Debug {@link #set} value when the VPN stats are moved in.
+     * @hide
+     */
+    public static final int SET_DBG_VPN_IN = 1001;
+    /**
+     * Debug {@link #set} value when the VPN stats are moved out of a vpn UID.
+     * @hide
+     */
+    public static final int SET_DBG_VPN_OUT = 1002;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "SET_" }, value = {
+            SET_ALL,
+            SET_DEFAULT,
+            SET_FOREGROUND,
+    })
+    public @interface State {
+    }
+
+    /**
+     * Include all interfaces when filtering
+     * @hide
+     */
+    public @Nullable static final String[] INTERFACES_ALL = null;
+
+    /** {@link #tag} value for total data across all tags. */
+    public static final int TAG_NONE = 0;
+
+    /** {@link #metered} value to account for all metered states. */
+    public static final int METERED_ALL = -1;
+    /** {@link #metered} value where native, unmetered data is accounted. */
+    public static final int METERED_NO = 0;
+    /** {@link #metered} value where metered data is accounted. */
+    public static final int METERED_YES = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "METERED_" }, value = {
+            METERED_ALL,
+            METERED_NO,
+            METERED_YES
+    })
+    public @interface Meteredness {
+    }
+
+
+    /** {@link #roaming} value to account for all roaming states. */
+    public static final int ROAMING_ALL = -1;
+    /** {@link #roaming} value where native, non-roaming data is accounted. */
+    public static final int ROAMING_NO = 0;
+    /** {@link #roaming} value where roaming data is accounted. */
+    public static final int ROAMING_YES = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "ROAMING_" }, value = {
+            ROAMING_ALL,
+            ROAMING_NO,
+            ROAMING_YES
+    })
+    public @interface Roaming {
+    }
+
+    /** {@link #onDefaultNetwork} value to account for all default network states. */
+    public static final int DEFAULT_NETWORK_ALL = -1;
+    /** {@link #onDefaultNetwork} value to account for usage while not the default network. */
+    public static final int DEFAULT_NETWORK_NO = 0;
+    /** {@link #onDefaultNetwork} value to account for usage while the default network. */
+    public static final int DEFAULT_NETWORK_YES = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "DEFAULT_NETWORK_" }, value = {
+            DEFAULT_NETWORK_ALL,
+            DEFAULT_NETWORK_NO,
+            DEFAULT_NETWORK_YES
+    })
+    public @interface DefaultNetwork {
+    }
+
+    /**
+     * Denotes a request for stats at the interface level.
+     * @hide
+     */
+    public static final int STATS_PER_IFACE = 0;
+    /**
+     * Denotes a request for stats at the interface and UID level.
+     * @hide
+     */
+    public static final int STATS_PER_UID = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "STATS_PER_" }, value = {
+            STATS_PER_IFACE,
+            STATS_PER_UID
+    })
+    public @interface StatsType {
+    }
+
+    private static final String CLATD_INTERFACE_PREFIX = "v4-";
+    // Delta between IPv4 header (20b) and IPv6 header (40b).
+    // Used for correct stats accounting on clatd interfaces.
+    private static final int IPV4V6_HEADER_DELTA = 20;
+
+    // TODO: move fields to "mVariable" notation
+
+    /**
+     * {@link SystemClock#elapsedRealtime()} timestamp in milliseconds when this data was
+     * generated.
+     * It's a timestamps delta when {@link #subtract()},
+     * {@code INetworkStatsSession#getSummaryForAllUid()} methods are used.
+     */
+    private long elapsedRealtime;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private int size;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private int capacity;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private String[] iface;
+    @UnsupportedAppUsage
+    private int[] uid;
+    @UnsupportedAppUsage
+    private int[] set;
+    @UnsupportedAppUsage
+    private int[] tag;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private int[] metered;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private int[] roaming;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private int[] defaultNetwork;
+    @UnsupportedAppUsage
+    private long[] rxBytes;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private long[] rxPackets;
+    @UnsupportedAppUsage
+    private long[] txBytes;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private long[] txPackets;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private long[] operations;
+
+    /**
+     * Basic element of network statistics. Contains the number of packets and number of bytes
+     * transferred on both directions in a given set of conditions. See
+     * {@link Entry#Entry(String, int, int, int, int, int, int, long, long, long, long, long)}.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static class Entry {
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public String iface;
+        /** @hide */
+        @UnsupportedAppUsage
+        public int uid;
+        /** @hide */
+        @UnsupportedAppUsage
+        public int set;
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public int tag;
+        /**
+         * Note that this is only populated w/ the default value when read from /proc or written
+         * to disk. We merge in the correct value when reporting this value to clients of
+         * getSummary().
+         * @hide
+         */
+        public int metered;
+        /**
+         * Note that this is only populated w/ the default value when read from /proc or written
+         * to disk. We merge in the correct value when reporting this value to clients of
+         * getSummary().
+         * @hide
+         */
+        public int roaming;
+        /**
+         * Note that this is only populated w/ the default value when read from /proc or written
+         * to disk. We merge in the correct value when reporting this value to clients of
+         * getSummary().
+         * @hide
+         */
+        public int defaultNetwork;
+        /** @hide */
+        @UnsupportedAppUsage
+        public long rxBytes;
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public long rxPackets;
+        /** @hide */
+        @UnsupportedAppUsage
+        public long txBytes;
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public long txPackets;
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public long operations;
+
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public Entry() {
+            this(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
+        }
+
+        /** @hide */
+        public Entry(long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+            this(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets,
+                    operations);
+        }
+
+        /** @hide */
+        public Entry(String iface, int uid, int set, int tag, long rxBytes, long rxPackets,
+                long txBytes, long txPackets, long operations) {
+            this(iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO,
+                    rxBytes, rxPackets, txBytes, txPackets, operations);
+        }
+
+        /**
+         * Construct a {@link Entry} object by giving statistics of packet and byte transferred on
+         * both direction, and associated with a set of given conditions.
+         *
+         * @param iface interface name of this {@link Entry}. Or null if not specified.
+         * @param uid uid of this {@link Entry}. {@link #UID_TETHERING} if this {@link Entry} is
+         *            for tethering. Or {@link #UID_ALL} if this {@link NetworkStats} is only
+         *            counting iface stats.
+         * @param set usage state of this {@link Entry}.
+         * @param tag tag of this {@link Entry}.
+         * @param metered metered state of this {@link Entry}.
+         * @param roaming roaming state of this {@link Entry}.
+         * @param defaultNetwork default network status of this {@link Entry}.
+         * @param rxBytes Number of bytes received for this {@link Entry}. Statistics should
+         *                represent the contents of IP packets, including IP headers.
+         * @param rxPackets Number of packets received for this {@link Entry}. Statistics should
+         *                  represent the contents of IP packets, including IP headers.
+         * @param txBytes Number of bytes transmitted for this {@link Entry}. Statistics should
+         *                represent the contents of IP packets, including IP headers.
+         * @param txPackets Number of bytes transmitted for this {@link Entry}. Statistics should
+         *                  represent the contents of IP packets, including IP headers.
+         * @param operations count of network operations performed for this {@link Entry}. This can
+         *                   be used to derive bytes-per-operation.
+         */
+        public Entry(@Nullable String iface, int uid, @State int set, int tag,
+                @Meteredness int metered, @Roaming int roaming, @DefaultNetwork int defaultNetwork,
+                long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+            this.iface = iface;
+            this.uid = uid;
+            this.set = set;
+            this.tag = tag;
+            this.metered = metered;
+            this.roaming = roaming;
+            this.defaultNetwork = defaultNetwork;
+            this.rxBytes = rxBytes;
+            this.rxPackets = rxPackets;
+            this.txBytes = txBytes;
+            this.txPackets = txPackets;
+            this.operations = operations;
+        }
+
+        /** @hide */
+        public boolean isNegative() {
+            return rxBytes < 0 || rxPackets < 0 || txBytes < 0 || txPackets < 0 || operations < 0;
+        }
+
+        /** @hide */
+        public boolean isEmpty() {
+            return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0
+                    && operations == 0;
+        }
+
+        /** @hide */
+        public void add(Entry another) {
+            this.rxBytes += another.rxBytes;
+            this.rxPackets += another.rxPackets;
+            this.txBytes += another.txBytes;
+            this.txPackets += another.txPackets;
+            this.operations += another.operations;
+        }
+
+        /**
+         * @return interface name of this entry.
+         * @hide
+         */
+        @Nullable public String getIface() {
+            return iface;
+        }
+
+        /**
+         * @return the uid of this entry.
+         */
+        public int getUid() {
+            return uid;
+        }
+
+        /**
+         * @return the set state of this entry.
+         */
+        @State public int getSet() {
+            return set;
+        }
+
+        /**
+         * @return the tag value of this entry.
+         */
+        public int getTag() {
+            return tag;
+        }
+
+        /**
+         * @return the metered state.
+         */
+        @Meteredness
+        public int getMetered() {
+            return metered;
+        }
+
+        /**
+         * @return the roaming state.
+         */
+        @Roaming
+        public int getRoaming() {
+            return roaming;
+        }
+
+        /**
+         * @return the default network state.
+         */
+        @DefaultNetwork
+        public int getDefaultNetwork() {
+            return defaultNetwork;
+        }
+
+        /**
+         * @return the number of received bytes.
+         */
+        public long getRxBytes() {
+            return rxBytes;
+        }
+
+        /**
+         * @return the number of received packets.
+         */
+        public long getRxPackets() {
+            return rxPackets;
+        }
+
+        /**
+         * @return the number of transmitted bytes.
+         */
+        public long getTxBytes() {
+            return txBytes;
+        }
+
+        /**
+         * @return the number of transmitted packets.
+         */
+        public long getTxPackets() {
+            return txPackets;
+        }
+
+        /**
+         * @return the count of network operations performed for this entry.
+         */
+        public long getOperations() {
+            return operations;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder builder = new StringBuilder();
+            builder.append("iface=").append(iface);
+            builder.append(" uid=").append(uid);
+            builder.append(" set=").append(setToString(set));
+            builder.append(" tag=").append(tagToString(tag));
+            builder.append(" metered=").append(meteredToString(metered));
+            builder.append(" roaming=").append(roamingToString(roaming));
+            builder.append(" defaultNetwork=").append(defaultNetworkToString(defaultNetwork));
+            builder.append(" rxBytes=").append(rxBytes);
+            builder.append(" rxPackets=").append(rxPackets);
+            builder.append(" txBytes=").append(txBytes);
+            builder.append(" txPackets=").append(txPackets);
+            builder.append(" operations=").append(operations);
+            return builder.toString();
+        }
+
+        /** @hide */
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (o instanceof Entry) {
+                final Entry e = (Entry) o;
+                return uid == e.uid && set == e.set && tag == e.tag && metered == e.metered
+                        && roaming == e.roaming && defaultNetwork == e.defaultNetwork
+                        && rxBytes == e.rxBytes && rxPackets == e.rxPackets
+                        && txBytes == e.txBytes && txPackets == e.txPackets
+                        && operations == e.operations && TextUtils.equals(iface, e.iface);
+            }
+            return false;
+        }
+
+        /** @hide */
+        @Override
+        public int hashCode() {
+            return Objects.hash(uid, set, tag, metered, roaming, defaultNetwork, iface);
+        }
+    }
+
+    public NetworkStats(long elapsedRealtime, int initialSize) {
+        this.elapsedRealtime = elapsedRealtime;
+        this.size = 0;
+        if (initialSize > 0) {
+            this.capacity = initialSize;
+            this.iface = new String[initialSize];
+            this.uid = new int[initialSize];
+            this.set = new int[initialSize];
+            this.tag = new int[initialSize];
+            this.metered = new int[initialSize];
+            this.roaming = new int[initialSize];
+            this.defaultNetwork = new int[initialSize];
+            this.rxBytes = new long[initialSize];
+            this.rxPackets = new long[initialSize];
+            this.txBytes = new long[initialSize];
+            this.txPackets = new long[initialSize];
+            this.operations = new long[initialSize];
+        } else {
+            // Special case for use by NetworkStatsFactory to start out *really* empty.
+            clear();
+        }
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public NetworkStats(Parcel parcel) {
+        elapsedRealtime = parcel.readLong();
+        size = parcel.readInt();
+        capacity = parcel.readInt();
+        iface = parcel.createStringArray();
+        uid = parcel.createIntArray();
+        set = parcel.createIntArray();
+        tag = parcel.createIntArray();
+        metered = parcel.createIntArray();
+        roaming = parcel.createIntArray();
+        defaultNetwork = parcel.createIntArray();
+        rxBytes = parcel.createLongArray();
+        rxPackets = parcel.createLongArray();
+        txBytes = parcel.createLongArray();
+        txPackets = parcel.createLongArray();
+        operations = parcel.createLongArray();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeLong(elapsedRealtime);
+        dest.writeInt(size);
+        dest.writeInt(capacity);
+        dest.writeStringArray(iface);
+        dest.writeIntArray(uid);
+        dest.writeIntArray(set);
+        dest.writeIntArray(tag);
+        dest.writeIntArray(metered);
+        dest.writeIntArray(roaming);
+        dest.writeIntArray(defaultNetwork);
+        dest.writeLongArray(rxBytes);
+        dest.writeLongArray(rxPackets);
+        dest.writeLongArray(txBytes);
+        dest.writeLongArray(txPackets);
+        dest.writeLongArray(operations);
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public NetworkStats clone() {
+        final NetworkStats clone = new NetworkStats(elapsedRealtime, size);
+        NetworkStats.Entry entry = null;
+        for (int i = 0; i < size; i++) {
+            entry = getValues(i, entry);
+            clone.insertEntry(entry);
+        }
+        return clone;
+    }
+
+    /**
+     * Clear all data stored in this object.
+     * @hide
+     */
+    public void clear() {
+        this.capacity = 0;
+        this.iface = EmptyArray.STRING;
+        this.uid = EmptyArray.INT;
+        this.set = EmptyArray.INT;
+        this.tag = EmptyArray.INT;
+        this.metered = EmptyArray.INT;
+        this.roaming = EmptyArray.INT;
+        this.defaultNetwork = EmptyArray.INT;
+        this.rxBytes = EmptyArray.LONG;
+        this.rxPackets = EmptyArray.LONG;
+        this.txBytes = EmptyArray.LONG;
+        this.txPackets = EmptyArray.LONG;
+        this.operations = EmptyArray.LONG;
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public NetworkStats insertEntry(
+            String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        return insertEntry(
+                iface, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, 0L);
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public NetworkStats insertEntry(String iface, int uid, int set, int tag, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, long operations) {
+        return insertEntry(new Entry(
+                iface, uid, set, tag, rxBytes, rxPackets, txBytes, txPackets, operations));
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public NetworkStats insertEntry(String iface, int uid, int set, int tag, int metered,
+            int roaming, int defaultNetwork, long rxBytes, long rxPackets, long txBytes,
+            long txPackets, long operations) {
+        return insertEntry(new Entry(
+                iface, uid, set, tag, metered, roaming, defaultNetwork, rxBytes, rxPackets,
+                txBytes, txPackets, operations));
+    }
+
+    /**
+     * Add new stats entry, copying from given {@link Entry}. The {@link Entry}
+     * object can be recycled across multiple calls.
+     * @hide
+     */
+    public NetworkStats insertEntry(Entry entry) {
+        if (size >= capacity) {
+            final int newLength = Math.max(size, 10) * 3 / 2;
+            iface = Arrays.copyOf(iface, newLength);
+            uid = Arrays.copyOf(uid, newLength);
+            set = Arrays.copyOf(set, newLength);
+            tag = Arrays.copyOf(tag, newLength);
+            metered = Arrays.copyOf(metered, newLength);
+            roaming = Arrays.copyOf(roaming, newLength);
+            defaultNetwork = Arrays.copyOf(defaultNetwork, newLength);
+            rxBytes = Arrays.copyOf(rxBytes, newLength);
+            rxPackets = Arrays.copyOf(rxPackets, newLength);
+            txBytes = Arrays.copyOf(txBytes, newLength);
+            txPackets = Arrays.copyOf(txPackets, newLength);
+            operations = Arrays.copyOf(operations, newLength);
+            capacity = newLength;
+        }
+
+        setValues(size, entry);
+        size++;
+
+        return this;
+    }
+
+    private void setValues(int i, Entry entry) {
+        iface[i] = entry.iface;
+        uid[i] = entry.uid;
+        set[i] = entry.set;
+        tag[i] = entry.tag;
+        metered[i] = entry.metered;
+        roaming[i] = entry.roaming;
+        defaultNetwork[i] = entry.defaultNetwork;
+        rxBytes[i] = entry.rxBytes;
+        rxPackets[i] = entry.rxPackets;
+        txBytes[i] = entry.txBytes;
+        txPackets[i] = entry.txPackets;
+        operations[i] = entry.operations;
+    }
+
+    /**
+     * Iterate over Entry objects.
+     *
+     * Return an iterator of this object that will iterate through all contained Entry objects.
+     *
+     * This iterator does not support concurrent modification and makes no guarantee of fail-fast
+     * behavior. If any method that can mutate the contents of this object is called while
+     * iteration is in progress, either inside the loop or in another thread, then behavior is
+     * undefined.
+     * The remove() method is not implemented and will throw UnsupportedOperationException.
+     * @hide
+     */
+    @SystemApi
+    @NonNull public Iterator<Entry> iterator() {
+        return new Iterator<Entry>() {
+            int mIndex = 0;
+
+            @Override
+            public boolean hasNext() {
+                return mIndex < size;
+            }
+
+            @Override
+            public Entry next() {
+                return getValues(mIndex++, null);
+            }
+        };
+    }
+
+    /**
+     * Return specific stats entry.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public Entry getValues(int i, @Nullable Entry recycle) {
+        final Entry entry = recycle != null ? recycle : new Entry();
+        entry.iface = iface[i];
+        entry.uid = uid[i];
+        entry.set = set[i];
+        entry.tag = tag[i];
+        entry.metered = metered[i];
+        entry.roaming = roaming[i];
+        entry.defaultNetwork = defaultNetwork[i];
+        entry.rxBytes = rxBytes[i];
+        entry.rxPackets = rxPackets[i];
+        entry.txBytes = txBytes[i];
+        entry.txPackets = txPackets[i];
+        entry.operations = operations[i];
+        return entry;
+    }
+
+    /**
+     * If @{code dest} is not equal to @{code src}, copy entry from index @{code src} to index
+     * @{code dest}.
+     */
+    private void maybeCopyEntry(int dest, int src) {
+        if (dest == src) return;
+        iface[dest] = iface[src];
+        uid[dest] = uid[src];
+        set[dest] = set[src];
+        tag[dest] = tag[src];
+        metered[dest] = metered[src];
+        roaming[dest] = roaming[src];
+        defaultNetwork[dest] = defaultNetwork[src];
+        rxBytes[dest] = rxBytes[src];
+        rxPackets[dest] = rxPackets[src];
+        txBytes[dest] = txBytes[src];
+        txPackets[dest] = txPackets[src];
+        operations[dest] = operations[src];
+    }
+
+    /** @hide */
+    public long getElapsedRealtime() {
+        return elapsedRealtime;
+    }
+
+    /** @hide */
+    public void setElapsedRealtime(long time) {
+        elapsedRealtime = time;
+    }
+
+    /**
+     * Return age of this {@link NetworkStats} object with respect to
+     * {@link SystemClock#elapsedRealtime()}.
+     * @hide
+     */
+    public long getElapsedRealtimeAge() {
+        return SystemClock.elapsedRealtime() - elapsedRealtime;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public int size() {
+        return size;
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public int internalSize() {
+        return capacity;
+    }
+
+    /** @hide */
+    @Deprecated
+    public NetworkStats combineValues(String iface, int uid, int tag, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, long operations) {
+        return combineValues(
+                iface, uid, SET_DEFAULT, tag, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+    }
+
+    /** @hide */
+    public NetworkStats combineValues(String iface, int uid, int set, int tag,
+            long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+        return combineValues(new Entry(
+                iface, uid, set, tag, rxBytes, rxPackets, txBytes, txPackets, operations));
+    }
+
+    /**
+     * Combine given values with an existing row, or create a new row if
+     * {@link #findIndex(String, int, int, int, int, int, int)} is unable to find match. Can
+     * also be used to subtract values from existing rows. This method mutates the referencing
+     * {@link NetworkStats} object.
+     *
+     * @param entry the {@link Entry} to combine.
+     * @return a reference to this mutated {@link NetworkStats} object.
+     * @hide
+     */
+    public @NonNull NetworkStats combineValues(@NonNull Entry entry) {
+        final int i = findIndex(entry.iface, entry.uid, entry.set, entry.tag, entry.metered,
+                entry.roaming, entry.defaultNetwork);
+        if (i == -1) {
+            // only create new entry when positive contribution
+            insertEntry(entry);
+        } else {
+            rxBytes[i] += entry.rxBytes;
+            rxPackets[i] += entry.rxPackets;
+            txBytes[i] += entry.txBytes;
+            txPackets[i] += entry.txPackets;
+            operations[i] += entry.operations;
+        }
+        return this;
+    }
+
+    /**
+     * Add given values with an existing row, or create a new row if
+     * {@link #findIndex(String, int, int, int, int, int, int)} is unable to find match. Can
+     * also be used to subtract values from existing rows.
+     *
+     * @param entry the {@link Entry} to add.
+     * @return a new constructed {@link NetworkStats} object that contains the result.
+     */
+    public @NonNull NetworkStats addEntry(@NonNull Entry entry) {
+        return this.clone().combineValues(entry);
+    }
+
+    /**
+     * Add the given {@link NetworkStats} objects.
+     *
+     * @return the sum of two objects.
+     */
+    public @NonNull NetworkStats add(@NonNull NetworkStats another) {
+        final NetworkStats ret = this.clone();
+        ret.combineAllValues(another);
+        return ret;
+    }
+
+    /**
+     * Combine all values from another {@link NetworkStats} into this object.
+     * @hide
+     */
+    public void combineAllValues(@NonNull NetworkStats another) {
+        NetworkStats.Entry entry = null;
+        for (int i = 0; i < another.size; i++) {
+            entry = another.getValues(i, entry);
+            combineValues(entry);
+        }
+    }
+
+    /**
+     * Find first stats index that matches the requested parameters.
+     * @hide
+     */
+    public int findIndex(String iface, int uid, int set, int tag, int metered, int roaming,
+            int defaultNetwork) {
+        for (int i = 0; i < size; i++) {
+            if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
+                    && metered == this.metered[i] && roaming == this.roaming[i]
+                    && defaultNetwork == this.defaultNetwork[i]
+                    && Objects.equals(iface, this.iface[i])) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Find first stats index that matches the requested parameters, starting
+     * search around the hinted index as an optimization.
+     * @hide
+     */
+    @VisibleForTesting
+    public int findIndexHinted(String iface, int uid, int set, int tag, int metered, int roaming,
+            int defaultNetwork, int hintIndex) {
+        for (int offset = 0; offset < size; offset++) {
+            final int halfOffset = offset / 2;
+
+            // search outwards from hint index, alternating forward and backward
+            final int i;
+            if (offset % 2 == 0) {
+                i = (hintIndex + halfOffset) % size;
+            } else {
+                i = (size + hintIndex - halfOffset - 1) % size;
+            }
+
+            if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
+                    && metered == this.metered[i] && roaming == this.roaming[i]
+                    && defaultNetwork == this.defaultNetwork[i]
+                    && Objects.equals(iface, this.iface[i])) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Splice in {@link #operations} from the given {@link NetworkStats} based
+     * on matching {@link #uid} and {@link #tag} rows. Ignores {@link #iface},
+     * since operation counts are at data layer.
+     * @hide
+     */
+    public void spliceOperationsFrom(NetworkStats stats) {
+        for (int i = 0; i < size; i++) {
+            final int j = stats.findIndex(iface[i], uid[i], set[i], tag[i], metered[i], roaming[i],
+                    defaultNetwork[i]);
+            if (j == -1) {
+                operations[i] = 0;
+            } else {
+                operations[i] = stats.operations[j];
+            }
+        }
+    }
+
+    /**
+     * Return list of unique interfaces known by this data structure.
+     * @hide
+     */
+    public String[] getUniqueIfaces() {
+        final HashSet<String> ifaces = new HashSet<String>();
+        for (String iface : this.iface) {
+            if (iface != IFACE_ALL) {
+                ifaces.add(iface);
+            }
+        }
+        return ifaces.toArray(new String[ifaces.size()]);
+    }
+
+    /**
+     * Return list of unique UIDs known by this data structure.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int[] getUniqueUids() {
+        final SparseBooleanArray uids = new SparseBooleanArray();
+        for (int uid : this.uid) {
+            uids.put(uid, true);
+        }
+
+        final int size = uids.size();
+        final int[] result = new int[size];
+        for (int i = 0; i < size; i++) {
+            result[i] = uids.keyAt(i);
+        }
+        return result;
+    }
+
+    /**
+     * Return total bytes represented by this snapshot object, usually used when
+     * checking if a {@link #subtract(NetworkStats)} delta passes a threshold.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public long getTotalBytes() {
+        final Entry entry = getTotal(null);
+        return entry.rxBytes + entry.txBytes;
+    }
+
+    /**
+     * Return total of all fields represented by this snapshot object.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public Entry getTotal(Entry recycle) {
+        return getTotal(recycle, null, UID_ALL, false);
+    }
+
+    /**
+     * Return total of all fields represented by this snapshot object matching
+     * the requested {@link #uid}.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public Entry getTotal(Entry recycle, int limitUid) {
+        return getTotal(recycle, null, limitUid, false);
+    }
+
+    /**
+     * Return total of all fields represented by this snapshot object matching
+     * the requested {@link #iface}.
+     * @hide
+     */
+    public Entry getTotal(Entry recycle, HashSet<String> limitIface) {
+        return getTotal(recycle, limitIface, UID_ALL, false);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public Entry getTotalIncludingTags(Entry recycle) {
+        return getTotal(recycle, null, UID_ALL, true);
+    }
+
+    /**
+     * Return total of all fields represented by this snapshot object matching
+     * the requested {@link #iface} and {@link #uid}.
+     *
+     * @param limitIface Set of {@link #iface} to include in total; or {@code
+     *            null} to include all ifaces.
+     */
+    private Entry getTotal(
+            Entry recycle, HashSet<String> limitIface, int limitUid, boolean includeTags) {
+        final Entry entry = recycle != null ? recycle : new Entry();
+
+        entry.iface = IFACE_ALL;
+        entry.uid = limitUid;
+        entry.set = SET_ALL;
+        entry.tag = TAG_NONE;
+        entry.metered = METERED_ALL;
+        entry.roaming = ROAMING_ALL;
+        entry.defaultNetwork = DEFAULT_NETWORK_ALL;
+        entry.rxBytes = 0;
+        entry.rxPackets = 0;
+        entry.txBytes = 0;
+        entry.txPackets = 0;
+        entry.operations = 0;
+
+        for (int i = 0; i < size; i++) {
+            final boolean matchesUid = (limitUid == UID_ALL) || (limitUid == uid[i]);
+            final boolean matchesIface = (limitIface == null) || (limitIface.contains(iface[i]));
+
+            if (matchesUid && matchesIface) {
+                // skip specific tags, since already counted in TAG_NONE
+                if (tag[i] != TAG_NONE && !includeTags) continue;
+
+                entry.rxBytes += rxBytes[i];
+                entry.rxPackets += rxPackets[i];
+                entry.txBytes += txBytes[i];
+                entry.txPackets += txPackets[i];
+                entry.operations += operations[i];
+            }
+        }
+        return entry;
+    }
+
+    /**
+     * Fast path for battery stats.
+     * @hide
+     */
+    public long getTotalPackets() {
+        long total = 0;
+        for (int i = size-1; i >= 0; i--) {
+            total += rxPackets[i] + txPackets[i];
+        }
+        return total;
+    }
+
+    /**
+     * Subtract the given {@link NetworkStats}, effectively leaving the delta
+     * between two snapshots in time. Assumes that statistics rows collect over
+     * time, and that none of them have disappeared. This method does not mutate
+     * the referencing object.
+     *
+     * @return the delta between two objects.
+     */
+    public @NonNull NetworkStats subtract(@NonNull NetworkStats right) {
+        return subtract(this, right, null, null);
+    }
+
+    /**
+     * Subtract the two given {@link NetworkStats} objects, returning the delta
+     * between two snapshots in time. Assumes that statistics rows collect over
+     * time, and that none of them have disappeared.
+     * <p>
+     * If counters have rolled backwards, they are clamped to {@code 0} and
+     * reported to the given {@link NonMonotonicObserver}.
+     * @hide
+     */
+    public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right,
+            NonMonotonicObserver<C> observer, C cookie) {
+        return subtract(left, right, observer, cookie, null);
+    }
+
+    /**
+     * Subtract the two given {@link NetworkStats} objects, returning the delta
+     * between two snapshots in time. Assumes that statistics rows collect over
+     * time, and that none of them have disappeared.
+     * <p>
+     * If counters have rolled backwards, they are clamped to {@code 0} and
+     * reported to the given {@link NonMonotonicObserver}.
+     * <p>
+     * If <var>recycle</var> is supplied, this NetworkStats object will be
+     * reused (and returned) as the result if it is large enough to contain
+     * the data.
+     * @hide
+     */
+    public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right,
+            NonMonotonicObserver<C> observer, C cookie, NetworkStats recycle) {
+        long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime;
+        if (deltaRealtime < 0) {
+            if (observer != null) {
+                observer.foundNonMonotonic(left, -1, right, -1, cookie);
+            }
+            deltaRealtime = 0;
+        }
+
+        // result will have our rows, and elapsed time between snapshots
+        final Entry entry = new Entry();
+        final NetworkStats result;
+        if (recycle != null && recycle.capacity >= left.size) {
+            result = recycle;
+            result.size = 0;
+            result.elapsedRealtime = deltaRealtime;
+        } else {
+            result = new NetworkStats(deltaRealtime, left.size);
+        }
+        for (int i = 0; i < left.size; i++) {
+            entry.iface = left.iface[i];
+            entry.uid = left.uid[i];
+            entry.set = left.set[i];
+            entry.tag = left.tag[i];
+            entry.metered = left.metered[i];
+            entry.roaming = left.roaming[i];
+            entry.defaultNetwork = left.defaultNetwork[i];
+            entry.rxBytes = left.rxBytes[i];
+            entry.rxPackets = left.rxPackets[i];
+            entry.txBytes = left.txBytes[i];
+            entry.txPackets = left.txPackets[i];
+            entry.operations = left.operations[i];
+
+            // find remote row that matches, and subtract
+            final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag,
+                    entry.metered, entry.roaming, entry.defaultNetwork, i);
+            if (j != -1) {
+                // Found matching row, subtract remote value.
+                entry.rxBytes -= right.rxBytes[j];
+                entry.rxPackets -= right.rxPackets[j];
+                entry.txBytes -= right.txBytes[j];
+                entry.txPackets -= right.txPackets[j];
+                entry.operations -= right.operations[j];
+            }
+
+            if (entry.isNegative()) {
+                if (observer != null) {
+                    observer.foundNonMonotonic(left, i, right, j, cookie);
+                }
+                entry.rxBytes = Math.max(entry.rxBytes, 0);
+                entry.rxPackets = Math.max(entry.rxPackets, 0);
+                entry.txBytes = Math.max(entry.txBytes, 0);
+                entry.txPackets = Math.max(entry.txPackets, 0);
+                entry.operations = Math.max(entry.operations, 0);
+            }
+
+            result.insertEntry(entry);
+        }
+
+        return result;
+    }
+
+    /**
+     * Calculate and apply adjustments to captured statistics for 464xlat traffic.
+     *
+     * <p>This mutates stacked traffic stats, to account for IPv4/IPv6 header size difference.
+     *
+     * <p>UID stats, which are only accounted on the stacked interface, need to be increased
+     * by 20 bytes/packet to account for translation overhead.
+     *
+     * <p>The potential additional overhead of 8 bytes/packet for ip fragments is ignored.
+     *
+     * <p>Interface stats need to sum traffic on both stacked and base interface because:
+     *   - eBPF offloaded packets appear only on the stacked interface
+     *   - Non-offloaded ingress packets appear only on the stacked interface
+     *     (due to iptables raw PREROUTING drop rules)
+     *   - Non-offloaded egress packets appear only on the stacked interface
+     *     (due to ignoring traffic from clat daemon by uid match)
+     * (and of course the 20 bytes/packet overhead needs to be applied to stacked interface stats)
+     *
+     * <p>This method will behave fine if {@code stackedIfaces} is an non-synchronized but add-only
+     * {@code ConcurrentHashMap}
+     * @param baseTraffic Traffic on the base interfaces. Will be mutated.
+     * @param stackedTraffic Stats with traffic stacked on top of our ifaces. Will also be mutated.
+     * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both.
+     * @hide
+     */
+    public static void apply464xlatAdjustments(NetworkStats baseTraffic,
+            NetworkStats stackedTraffic, Map<String, String> stackedIfaces) {
+        // For recycling
+        Entry entry = null;
+        for (int i = 0; i < stackedTraffic.size; i++) {
+            entry = stackedTraffic.getValues(i, entry);
+            if (entry == null) continue;
+            if (entry.iface == null) continue;
+            if (!entry.iface.startsWith(CLATD_INTERFACE_PREFIX)) continue;
+
+            // For 464xlat traffic, per uid stats only counts the bytes of the native IPv4 packet
+            // sent on the stacked interface with prefix "v4-" and drops the IPv6 header size after
+            // unwrapping. To account correctly for on-the-wire traffic, add the 20 additional bytes
+            // difference for all packets (http://b/12249687, http:/b/33681750).
+            //
+            // Note: this doesn't account for LRO/GRO/GSO/TSO (ie. >mtu) traffic correctly, nor
+            // does it correctly account for the 8 extra bytes in the IPv6 fragmentation header.
+            //
+            // While the ebpf code path does try to simulate proper post segmentation packet
+            // counts, we have nothing of the sort of xt_qtaguid stats.
+            entry.rxBytes += entry.rxPackets * IPV4V6_HEADER_DELTA;
+            entry.txBytes += entry.txPackets * IPV4V6_HEADER_DELTA;
+            stackedTraffic.setValues(i, entry);
+        }
+    }
+
+    /**
+     * Calculate and apply adjustments to captured statistics for 464xlat traffic counted twice.
+     *
+     * <p>This mutates the object this method is called on. Equivalent to calling
+     * {@link #apply464xlatAdjustments(NetworkStats, NetworkStats, Map)} with {@code this} as
+     * base and stacked traffic.
+     * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both.
+     * @hide
+     */
+    public void apply464xlatAdjustments(Map<String, String> stackedIfaces) {
+        apply464xlatAdjustments(this, this, stackedIfaces);
+    }
+
+    /**
+     * Return total statistics grouped by {@link #iface}; doesn't mutate the
+     * original structure.
+     * @hide
+     */
+    public NetworkStats groupedByIface() {
+        final NetworkStats stats = new NetworkStats(elapsedRealtime, 10);
+
+        final Entry entry = new Entry();
+        entry.uid = UID_ALL;
+        entry.set = SET_ALL;
+        entry.tag = TAG_NONE;
+        entry.metered = METERED_ALL;
+        entry.roaming = ROAMING_ALL;
+        entry.defaultNetwork = DEFAULT_NETWORK_ALL;
+        entry.operations = 0L;
+
+        for (int i = 0; i < size; i++) {
+            // skip specific tags, since already counted in TAG_NONE
+            if (tag[i] != TAG_NONE) continue;
+
+            entry.iface = iface[i];
+            entry.rxBytes = rxBytes[i];
+            entry.rxPackets = rxPackets[i];
+            entry.txBytes = txBytes[i];
+            entry.txPackets = txPackets[i];
+            stats.combineValues(entry);
+        }
+
+        return stats;
+    }
+
+    /**
+     * Return total statistics grouped by {@link #uid}; doesn't mutate the
+     * original structure.
+     * @hide
+     */
+    public NetworkStats groupedByUid() {
+        final NetworkStats stats = new NetworkStats(elapsedRealtime, 10);
+
+        final Entry entry = new Entry();
+        entry.iface = IFACE_ALL;
+        entry.set = SET_ALL;
+        entry.tag = TAG_NONE;
+        entry.metered = METERED_ALL;
+        entry.roaming = ROAMING_ALL;
+        entry.defaultNetwork = DEFAULT_NETWORK_ALL;
+
+        for (int i = 0; i < size; i++) {
+            // skip specific tags, since already counted in TAG_NONE
+            if (tag[i] != TAG_NONE) continue;
+
+            entry.uid = uid[i];
+            entry.rxBytes = rxBytes[i];
+            entry.rxPackets = rxPackets[i];
+            entry.txBytes = txBytes[i];
+            entry.txPackets = txPackets[i];
+            entry.operations = operations[i];
+            stats.combineValues(entry);
+        }
+
+        return stats;
+    }
+
+    /**
+     * Remove all rows that match one of specified UIDs.
+     * This mutates the original structure in place.
+     * @hide
+     */
+    public void removeUids(int[] uids) {
+        filter(e -> !CollectionUtils.contains(uids, e.uid));
+    }
+
+    /**
+     * Remove all rows that match one of specified UIDs.
+     * @return the result object.
+     * @hide
+     */
+    @NonNull
+    public NetworkStats removeEmptyEntries() {
+        final NetworkStats ret = this.clone();
+        ret.filter(e -> e.rxBytes != 0 || e.rxPackets != 0 || e.txBytes != 0 || e.txPackets != 0
+                || e.operations != 0);
+        return ret;
+    }
+
+    /**
+     * Only keep entries that match all specified filters.
+     *
+     * <p>This mutates the original structure in place. After this method is called,
+     * size is the number of matching entries, and capacity is the previous capacity.
+     * @param limitUid UID to filter for, or {@link #UID_ALL}.
+     * @param limitIfaces Interfaces to filter for, or {@link #INTERFACES_ALL}.
+     * @param limitTag Tag to filter for, or {@link #TAG_ALL}.
+     * @hide
+     */
+    public void filter(int limitUid, String[] limitIfaces, int limitTag) {
+        if (limitUid == UID_ALL && limitTag == TAG_ALL && limitIfaces == INTERFACES_ALL) {
+            return;
+        }
+        filter(e -> (limitUid == UID_ALL || limitUid == e.uid)
+                && (limitTag == TAG_ALL || limitTag == e.tag)
+                && (limitIfaces == INTERFACES_ALL
+                    || CollectionUtils.contains(limitIfaces, e.iface)));
+    }
+
+    /**
+     * Only keep entries with {@link #set} value less than {@link #SET_DEBUG_START}.
+     *
+     * <p>This mutates the original structure in place.
+     * @hide
+     */
+    public void filterDebugEntries() {
+        filter(e -> e.set < SET_DEBUG_START);
+    }
+
+    private void filter(Predicate<Entry> predicate) {
+        Entry entry = new Entry();
+        int nextOutputEntry = 0;
+        for (int i = 0; i < size; i++) {
+            entry = getValues(i, entry);
+            if (predicate.test(entry)) {
+                if (nextOutputEntry != i) {
+                    setValues(nextOutputEntry, entry);
+                }
+                nextOutputEntry++;
+            }
+        }
+        size = nextOutputEntry;
+    }
+
+    /** @hide */
+    public void dump(String prefix, PrintWriter pw) {
+        pw.print(prefix);
+        pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime);
+        for (int i = 0; i < size; i++) {
+            pw.print(prefix);
+            pw.print("  ["); pw.print(i); pw.print("]");
+            pw.print(" iface="); pw.print(iface[i]);
+            pw.print(" uid="); pw.print(uid[i]);
+            pw.print(" set="); pw.print(setToString(set[i]));
+            pw.print(" tag="); pw.print(tagToString(tag[i]));
+            pw.print(" metered="); pw.print(meteredToString(metered[i]));
+            pw.print(" roaming="); pw.print(roamingToString(roaming[i]));
+            pw.print(" defaultNetwork="); pw.print(defaultNetworkToString(defaultNetwork[i]));
+            pw.print(" rxBytes="); pw.print(rxBytes[i]);
+            pw.print(" rxPackets="); pw.print(rxPackets[i]);
+            pw.print(" txBytes="); pw.print(txBytes[i]);
+            pw.print(" txPackets="); pw.print(txPackets[i]);
+            pw.print(" operations="); pw.println(operations[i]);
+        }
+    }
+
+    /**
+     * Return text description of {@link #set} value.
+     * @hide
+     */
+    public static String setToString(int set) {
+        switch (set) {
+            case SET_ALL:
+                return "ALL";
+            case SET_DEFAULT:
+                return "DEFAULT";
+            case SET_FOREGROUND:
+                return "FOREGROUND";
+            case SET_DBG_VPN_IN:
+                return "DBG_VPN_IN";
+            case SET_DBG_VPN_OUT:
+                return "DBG_VPN_OUT";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Return text description of {@link #set} value.
+     * @hide
+     */
+    public static String setToCheckinString(int set) {
+        switch (set) {
+            case SET_ALL:
+                return "all";
+            case SET_DEFAULT:
+                return "def";
+            case SET_FOREGROUND:
+                return "fg";
+            case SET_DBG_VPN_IN:
+                return "vpnin";
+            case SET_DBG_VPN_OUT:
+                return "vpnout";
+            default:
+                return "unk";
+        }
+    }
+
+    /**
+     * @return true if the querySet matches the dataSet.
+     * @hide
+     */
+    public static boolean setMatches(int querySet, int dataSet) {
+        if (querySet == dataSet) {
+            return true;
+        }
+        // SET_ALL matches all non-debugging sets.
+        return querySet == SET_ALL && dataSet < SET_DEBUG_START;
+    }
+
+    /**
+     * Return text description of {@link #tag} value.
+     * @hide
+     */
+    public static String tagToString(int tag) {
+        return "0x" + Integer.toHexString(tag);
+    }
+
+    /**
+     * Return text description of {@link #metered} value.
+     * @hide
+     */
+    public static String meteredToString(int metered) {
+        switch (metered) {
+            case METERED_ALL:
+                return "ALL";
+            case METERED_NO:
+                return "NO";
+            case METERED_YES:
+                return "YES";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Return text description of {@link #roaming} value.
+     * @hide
+     */
+    public static String roamingToString(int roaming) {
+        switch (roaming) {
+            case ROAMING_ALL:
+                return "ALL";
+            case ROAMING_NO:
+                return "NO";
+            case ROAMING_YES:
+                return "YES";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Return text description of {@link #defaultNetwork} value.
+     * @hide
+     */
+    public static String defaultNetworkToString(int defaultNetwork) {
+        switch (defaultNetwork) {
+            case DEFAULT_NETWORK_ALL:
+                return "ALL";
+            case DEFAULT_NETWORK_NO:
+                return "NO";
+            case DEFAULT_NETWORK_YES:
+                return "YES";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /** @hide */
+    @Override
+    public String toString() {
+        final CharArrayWriter writer = new CharArrayWriter();
+        dump("", new PrintWriter(writer));
+        return writer.toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<NetworkStats> CREATOR = new Creator<NetworkStats>() {
+        @Override
+        public NetworkStats createFromParcel(Parcel in) {
+            return new NetworkStats(in);
+        }
+
+        @Override
+        public NetworkStats[] newArray(int size) {
+            return new NetworkStats[size];
+        }
+    };
+
+    /** @hide */
+    public interface NonMonotonicObserver<C> {
+        public void foundNonMonotonic(
+                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie);
+        public void foundNonMonotonic(
+                NetworkStats stats, int statsIndex, C cookie);
+    }
+
+    /**
+     * VPN accounting. Move some VPN's underlying traffic to other UIDs that use tun0 iface.
+     *
+     * <p>This method should only be called on delta NetworkStats. Do not call this method on a
+     * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may change
+     * over time.
+     *
+     * <p>This method performs adjustments for one active VPN package and one VPN iface at a time.
+     *
+     * @param tunUid uid of the VPN application
+     * @param tunIface iface of the vpn tunnel
+     * @param underlyingIfaces underlying network ifaces used by the VPN application
+     * @hide
+     */
+    public void migrateTun(int tunUid, @NonNull String tunIface,
+            @NonNull List<String> underlyingIfaces) {
+        // Combined usage by all apps using VPN.
+        final Entry tunIfaceTotal = new Entry();
+        // Usage by VPN, grouped by its {@code underlyingIfaces}.
+        final Entry[] perInterfaceTotal = new Entry[underlyingIfaces.size()];
+        // Usage by VPN, summed across all its {@code underlyingIfaces}.
+        final Entry underlyingIfacesTotal = new Entry();
+
+        for (int i = 0; i < perInterfaceTotal.length; i++) {
+            perInterfaceTotal[i] = new Entry();
+        }
+
+        tunAdjustmentInit(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, perInterfaceTotal,
+                underlyingIfacesTotal);
+
+        // If tunIface < underlyingIfacesTotal, it leaves the overhead traffic in the VPN app.
+        // If tunIface > underlyingIfacesTotal, the VPN app doesn't get credit for data compression.
+        // Negative stats should be avoided.
+        final Entry[] moved =
+                addTrafficToApplications(tunUid, tunIface, underlyingIfaces, tunIfaceTotal,
+                        perInterfaceTotal, underlyingIfacesTotal);
+        deductTrafficFromVpnApp(tunUid, underlyingIfaces, moved);
+    }
+
+    /**
+     * Initializes the data used by the migrateTun() method.
+     *
+     * <p>This is the first pass iteration which does the following work:
+     *
+     * <ul>
+     *   <li>Adds up all the traffic through the tunUid's underlyingIfaces (both foreground and
+     *       background).
+     *   <li>Adds up all the traffic through tun0 excluding traffic from the vpn app itself.
+     * </ul>
+     *
+     * @param tunUid uid of the VPN application
+     * @param tunIface iface of the vpn tunnel
+     * @param underlyingIfaces underlying network ifaces used by the VPN application
+     * @param tunIfaceTotal output parameter; combined data usage by all apps using VPN
+     * @param perInterfaceTotal output parameter; data usage by VPN app, grouped by its {@code
+     *     underlyingIfaces}
+     * @param underlyingIfacesTotal output parameter; data usage by VPN, summed across all of its
+     *     {@code underlyingIfaces}
+     */
+    private void tunAdjustmentInit(int tunUid, @NonNull String tunIface,
+            @NonNull List<String> underlyingIfaces, @NonNull Entry tunIfaceTotal,
+            @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) {
+        final Entry recycle = new Entry();
+        for (int i = 0; i < size; i++) {
+            getValues(i, recycle);
+            if (recycle.uid == UID_ALL) {
+                throw new IllegalStateException(
+                        "Cannot adjust VPN accounting on an iface aggregated NetworkStats.");
+            }
+            if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) {
+                throw new IllegalStateException(
+                        "Cannot adjust VPN accounting on a NetworkStats containing SET_DBG_VPN_*");
+            }
+            if (recycle.tag != TAG_NONE) {
+                // TODO(b/123666283): Take all tags for tunUid into account.
+                continue;
+            }
+
+            if (tunUid == Process.SYSTEM_UID) {
+                // Kernel-based VPN or VCN, traffic sent by apps on the VPN/VCN network
+                //
+                // Since the data is not UID-accounted on underlying networks, just use VPN/VCN
+                // network usage as ground truth. Encrypted traffic on the underlying networks will
+                // never be processed here because encrypted traffic on the underlying interfaces
+                // is not present in UID stats, and this method is only called on UID stats.
+                if (tunIface.equals(recycle.iface)) {
+                    tunIfaceTotal.add(recycle);
+                    underlyingIfacesTotal.add(recycle);
+
+                    // In steady state, there should always be one network, but edge cases may
+                    // result in the network being null (network lost), and thus no underlying
+                    // ifaces is possible.
+                    if (perInterfaceTotal.length > 0) {
+                        // While platform VPNs and VCNs have exactly one underlying network, that
+                        // network may have multiple interfaces (eg for 464xlat). This layer does
+                        // not have the required information to identify which of the interfaces
+                        // were used. Select "any" of the interfaces. Since overhead is already
+                        // lost, this number is an approximation anyways.
+                        perInterfaceTotal[0].add(recycle);
+                    }
+                }
+            } else if (recycle.uid == tunUid) {
+                // VpnService VPN, traffic sent by the VPN app over underlying networks
+                for (int j = 0; j < underlyingIfaces.size(); j++) {
+                    if (Objects.equals(underlyingIfaces.get(j), recycle.iface)) {
+                        perInterfaceTotal[j].add(recycle);
+                        underlyingIfacesTotal.add(recycle);
+                        break;
+                    }
+                }
+            } else if (tunIface.equals(recycle.iface)) {
+                // VpnService VPN; traffic sent by apps on the VPN network
+                tunIfaceTotal.add(recycle);
+            }
+        }
+    }
+
+    /**
+     * Distributes traffic across apps that are using given {@code tunIface}, and returns the total
+     * traffic that should be moved off of {@code tunUid} grouped by {@code underlyingIfaces}.
+     *
+     * @param tunUid uid of the VPN application
+     * @param tunIface iface of the vpn tunnel
+     * @param underlyingIfaces underlying network ifaces used by the VPN application
+     * @param tunIfaceTotal combined data usage across all apps using {@code tunIface}
+     * @param perInterfaceTotal data usage by VPN app, grouped by its {@code underlyingIfaces}
+     * @param underlyingIfacesTotal data usage by VPN, summed across all of its {@code
+     *     underlyingIfaces}
+     */
+    private Entry[] addTrafficToApplications(int tunUid, @NonNull String tunIface,
+            @NonNull List<String> underlyingIfaces, @NonNull Entry tunIfaceTotal,
+            @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) {
+        // Traffic that should be moved off of each underlying interface for tunUid (see
+        // deductTrafficFromVpnApp below).
+        final Entry[] moved = new Entry[underlyingIfaces.size()];
+        for (int i = 0; i < underlyingIfaces.size(); i++) {
+            moved[i] = new Entry();
+        }
+
+        final Entry tmpEntry = new Entry();
+        final int origSize = size;
+        for (int i = 0; i < origSize; i++) {
+            if (!Objects.equals(iface[i], tunIface)) {
+                // Consider only entries that go onto the VPN interface.
+                continue;
+            }
+
+            if (uid[i] == tunUid && tunUid != Process.SYSTEM_UID) {
+                // Exclude VPN app from the redistribution, as it can choose to create packet
+                // streams by writing to itself.
+                //
+                // However, for platform VPNs, do not exclude the system's usage of the VPN network,
+                // since it is never local-only, and never double counted
+                continue;
+            }
+            tmpEntry.uid = uid[i];
+            tmpEntry.tag = tag[i];
+            tmpEntry.metered = metered[i];
+            tmpEntry.roaming = roaming[i];
+            tmpEntry.defaultNetwork = defaultNetwork[i];
+
+            // In a first pass, compute this entry's total share of data across all
+            // underlyingIfaces. This is computed on the basis of the share of this entry's usage
+            // over tunIface.
+            // TODO: Consider refactoring first pass into a separate helper method.
+            long totalRxBytes = 0;
+            if (tunIfaceTotal.rxBytes > 0) {
+                // Note - The multiplication below should not overflow since NetworkStatsService
+                // processes this every time device has transmitted/received amount equivalent to
+                // global threshold alert (~ 2MB) across all interfaces.
+                final long rxBytesAcrossUnderlyingIfaces =
+                        multiplySafeByRational(underlyingIfacesTotal.rxBytes,
+                                rxBytes[i], tunIfaceTotal.rxBytes);
+                // app must not be blamed for more than it consumed on tunIface
+                totalRxBytes = Math.min(rxBytes[i], rxBytesAcrossUnderlyingIfaces);
+            }
+            long totalRxPackets = 0;
+            if (tunIfaceTotal.rxPackets > 0) {
+                final long rxPacketsAcrossUnderlyingIfaces =
+                        multiplySafeByRational(underlyingIfacesTotal.rxPackets,
+                                rxPackets[i], tunIfaceTotal.rxPackets);
+                totalRxPackets = Math.min(rxPackets[i], rxPacketsAcrossUnderlyingIfaces);
+            }
+            long totalTxBytes = 0;
+            if (tunIfaceTotal.txBytes > 0) {
+                final long txBytesAcrossUnderlyingIfaces =
+                        multiplySafeByRational(underlyingIfacesTotal.txBytes,
+                                txBytes[i], tunIfaceTotal.txBytes);
+                totalTxBytes = Math.min(txBytes[i], txBytesAcrossUnderlyingIfaces);
+            }
+            long totalTxPackets = 0;
+            if (tunIfaceTotal.txPackets > 0) {
+                final long txPacketsAcrossUnderlyingIfaces =
+                        multiplySafeByRational(underlyingIfacesTotal.txPackets,
+                                txPackets[i], tunIfaceTotal.txPackets);
+                totalTxPackets = Math.min(txPackets[i], txPacketsAcrossUnderlyingIfaces);
+            }
+            long totalOperations = 0;
+            if (tunIfaceTotal.operations > 0) {
+                final long operationsAcrossUnderlyingIfaces =
+                        multiplySafeByRational(underlyingIfacesTotal.operations,
+                                operations[i], tunIfaceTotal.operations);
+                totalOperations = Math.min(operations[i], operationsAcrossUnderlyingIfaces);
+            }
+            // In a second pass, distribute these values across interfaces in the proportion that
+            // each interface represents of the total traffic of the underlying interfaces.
+            for (int j = 0; j < underlyingIfaces.size(); j++) {
+                tmpEntry.iface = underlyingIfaces.get(j);
+                tmpEntry.rxBytes = 0;
+                // Reset 'set' to correct value since it gets updated when adding debug info below.
+                tmpEntry.set = set[i];
+                if (underlyingIfacesTotal.rxBytes > 0) {
+                    tmpEntry.rxBytes =
+                            multiplySafeByRational(totalRxBytes,
+                                    perInterfaceTotal[j].rxBytes,
+                                    underlyingIfacesTotal.rxBytes);
+                }
+                tmpEntry.rxPackets = 0;
+                if (underlyingIfacesTotal.rxPackets > 0) {
+                    tmpEntry.rxPackets =
+                            multiplySafeByRational(totalRxPackets,
+                                    perInterfaceTotal[j].rxPackets,
+                                    underlyingIfacesTotal.rxPackets);
+                }
+                tmpEntry.txBytes = 0;
+                if (underlyingIfacesTotal.txBytes > 0) {
+                    tmpEntry.txBytes =
+                            multiplySafeByRational(totalTxBytes,
+                                    perInterfaceTotal[j].txBytes,
+                                    underlyingIfacesTotal.txBytes);
+                }
+                tmpEntry.txPackets = 0;
+                if (underlyingIfacesTotal.txPackets > 0) {
+                    tmpEntry.txPackets =
+                            multiplySafeByRational(totalTxPackets,
+                                    perInterfaceTotal[j].txPackets,
+                                    underlyingIfacesTotal.txPackets);
+                }
+                tmpEntry.operations = 0;
+                if (underlyingIfacesTotal.operations > 0) {
+                    tmpEntry.operations =
+                            multiplySafeByRational(totalOperations,
+                                    perInterfaceTotal[j].operations,
+                                    underlyingIfacesTotal.operations);
+                }
+                // tmpEntry now contains the migrated data of the i-th entry for the j-th underlying
+                // interface. Add that data usage to this object.
+                combineValues(tmpEntry);
+                if (tag[i] == TAG_NONE) {
+                    // Add the migrated data to moved so it is deducted from the VPN app later.
+                    moved[j].add(tmpEntry);
+                    // Add debug info
+                    tmpEntry.set = SET_DBG_VPN_IN;
+                    combineValues(tmpEntry);
+                }
+            }
+        }
+        return moved;
+    }
+
+    private void deductTrafficFromVpnApp(
+            int tunUid,
+            @NonNull List<String> underlyingIfaces,
+            @NonNull Entry[] moved) {
+        if (tunUid == Process.SYSTEM_UID) {
+            // No traffic recorded on a per-UID basis for in-kernel VPN/VCNs over underlying
+            // networks; thus no traffic to deduct.
+            return;
+        }
+
+        for (int i = 0; i < underlyingIfaces.size(); i++) {
+            moved[i].uid = tunUid;
+            // Add debug info
+            moved[i].set = SET_DBG_VPN_OUT;
+            moved[i].tag = TAG_NONE;
+            moved[i].iface = underlyingIfaces.get(i);
+            moved[i].metered = METERED_ALL;
+            moved[i].roaming = ROAMING_ALL;
+            moved[i].defaultNetwork = DEFAULT_NETWORK_ALL;
+            combineValues(moved[i]);
+
+            // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than
+            // the TAG_NONE traffic.
+            //
+            // Relies on the fact that the underlying traffic only has state ROAMING_NO and
+            // METERED_NO, which should be the case as it comes directly from the /proc file.
+            // We only blend in the roaming data after applying these adjustments, by checking the
+            // NetworkIdentity of the underlying iface.
+            final int idxVpnBackground = findIndex(underlyingIfaces.get(i), tunUid, SET_DEFAULT,
+                            TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
+            if (idxVpnBackground != -1) {
+                // Note - tunSubtract also updates moved[i]; whatever traffic that's left is removed
+                // from foreground usage.
+                tunSubtract(idxVpnBackground, this, moved[i]);
+            }
+
+            final int idxVpnForeground = findIndex(underlyingIfaces.get(i), tunUid, SET_FOREGROUND,
+                            TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
+            if (idxVpnForeground != -1) {
+                tunSubtract(idxVpnForeground, this, moved[i]);
+            }
+        }
+    }
+
+    private static void tunSubtract(int i, @NonNull NetworkStats left, @NonNull Entry right) {
+        long rxBytes = Math.min(left.rxBytes[i], right.rxBytes);
+        left.rxBytes[i] -= rxBytes;
+        right.rxBytes -= rxBytes;
+
+        long rxPackets = Math.min(left.rxPackets[i], right.rxPackets);
+        left.rxPackets[i] -= rxPackets;
+        right.rxPackets -= rxPackets;
+
+        long txBytes = Math.min(left.txBytes[i], right.txBytes);
+        left.txBytes[i] -= txBytes;
+        right.txBytes -= txBytes;
+
+        long txPackets = Math.min(left.txPackets[i], right.txPackets);
+        left.txPackets[i] -= txPackets;
+        right.txPackets -= txPackets;
+    }
+}
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
new file mode 100644
index 0000000..b64fbdb
--- /dev/null
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.UID_REMOVED;
+import static android.net.TrafficStats.UID_TETHERING;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.app.AppOpsManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Utility methods for controlling access to network stats APIs.
+ *
+ * @hide
+ */
+public final class NetworkStatsAccess {
+    private NetworkStatsAccess() {}
+
+    /**
+     * Represents an access level for the network usage history and statistics APIs.
+     *
+     * <p>Access levels are in increasing order; that is, it is reasonable to check access by
+     * verifying that the caller's access level is at least the minimum required level.
+     */
+    @IntDef({
+            Level.DEFAULT,
+            Level.USER,
+            Level.DEVICESUMMARY,
+            Level.DEVICE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Level {
+        /**
+         * Default, unprivileged access level.
+         *
+         * <p>Can only access usage for one's own UID.
+         *
+         * <p>Every app will have at least this access level.
+         */
+        int DEFAULT = 0;
+
+        /**
+         * Access level for apps which can access usage for any app running in the same user.
+         *
+         * <p>Granted to:
+         * <ul>
+         * <li>Profile owners.
+         * </ul>
+         */
+        int USER = 1;
+
+        /**
+         * Access level for apps which can access usage summary of device. Device summary includes
+         * usage by apps running in any profiles/users, however this access level does not
+         * allow querying usage of individual apps running in other profiles/users.
+         *
+         * <p>Granted to:
+         * <ul>
+         * <li>Apps with the PACKAGE_USAGE_STATS permission granted. Note that this is an AppOps bit
+         * so it is not necessarily sufficient to declare this in the manifest.
+         * <li>Apps with the (signature/privileged) READ_NETWORK_USAGE_HISTORY permission.
+         * </ul>
+         */
+        int DEVICESUMMARY = 2;
+
+        /**
+         * Access level for apps which can access usage for any app on the device, including apps
+         * running on other users/profiles.
+         *
+         * <p>Granted to:
+         * <ul>
+         * <li>Device owners.
+         * <li>Carrier-privileged applications.
+         * <li>The system UID.
+         * </ul>
+         */
+        int DEVICE = 3;
+    }
+
+    /** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
+    public static @NetworkStatsAccess.Level int checkAccessLevel(
+            Context context, int callingPid, int callingUid, String callingPackage) {
+        final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
+        final TelephonyManager tm = (TelephonyManager)
+                context.getSystemService(Context.TELEPHONY_SERVICE);
+        boolean hasCarrierPrivileges;
+        final long token = Binder.clearCallingIdentity();
+        try {
+            hasCarrierPrivileges = tm != null
+                    && tm.checkCarrierPrivilegesForPackageAnyPhone(callingPackage)
+                            == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        final boolean isDeviceOwner = mDpm != null && mDpm.isDeviceOwnerApp(callingPackage);
+        final int appId = UserHandle.getAppId(callingUid);
+
+        final boolean isNetworkStack = context.checkPermission(
+                android.Manifest.permission.NETWORK_STACK, callingPid, callingUid)
+                == PERMISSION_GRANTED;
+
+        if (hasCarrierPrivileges || isDeviceOwner
+                || appId == Process.SYSTEM_UID || isNetworkStack) {
+            // Carrier-privileged apps and device owners, and the system (including the
+            // network stack) can access data usage for all apps on the device.
+            return NetworkStatsAccess.Level.DEVICE;
+        }
+
+        boolean hasAppOpsPermission = hasAppOpsPermission(context, callingUid, callingPackage);
+        if (hasAppOpsPermission || context.checkCallingOrSelfPermission(
+                READ_NETWORK_USAGE_HISTORY) == PackageManager.PERMISSION_GRANTED) {
+            return NetworkStatsAccess.Level.DEVICESUMMARY;
+        }
+
+        //TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode.
+        boolean isProfileOwner = mDpm != null && (mDpm.isProfileOwnerApp(callingPackage)
+                || mDpm.isDeviceOwnerApp(callingPackage));
+        if (isProfileOwner) {
+            // Apps with the AppOps permission, profile owners, and apps with the privileged
+            // permission can access data usage for all apps in this user/profile.
+            return NetworkStatsAccess.Level.USER;
+        }
+
+        // Everyone else gets default access (only to their own UID).
+        return NetworkStatsAccess.Level.DEFAULT;
+    }
+
+    /**
+     * Returns whether the given caller should be able to access the given UID when the caller has
+     * the given {@link NetworkStatsAccess.Level}.
+     */
+    public static boolean isAccessibleToUser(int uid, int callerUid,
+            @NetworkStatsAccess.Level int accessLevel) {
+        final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+        final int callerUserId = UserHandle.getUserHandleForUid(callerUid).getIdentifier();
+        switch (accessLevel) {
+            case NetworkStatsAccess.Level.DEVICE:
+                // Device-level access - can access usage for any uid.
+                return true;
+            case NetworkStatsAccess.Level.DEVICESUMMARY:
+                // Can access usage for any app running in the same user, along
+                // with some special uids (system, removed, or tethering) and
+                // anonymized uids
+                return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED
+                        || uid == UID_TETHERING || uid == UID_ALL
+                        || userId == callerUserId;
+            case NetworkStatsAccess.Level.USER:
+                // User-level access - can access usage for any app running in the same user, along
+                // with some special uids (system, removed, or tethering).
+                return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED
+                        || uid == UID_TETHERING
+                        || userId == callerUserId;
+            case NetworkStatsAccess.Level.DEFAULT:
+            default:
+                // Default access level - can only access one's own usage.
+                return uid == callerUid;
+        }
+    }
+
+    private static boolean hasAppOpsPermission(
+            Context context, int callingUid, String callingPackage) {
+        if (callingPackage != null) {
+            AppOpsManager appOps = (AppOpsManager) context.getSystemService(
+                    Context.APP_OPS_SERVICE);
+
+            final int mode = appOps.noteOp(AppOpsManager.OPSTR_GET_USAGE_STATS,
+                    callingUid, callingPackage, null /* attributionTag */, null /* message */);
+            if (mode == AppOpsManager.MODE_DEFAULT) {
+                // The default behavior here is to check if PackageManager has given the app
+                // permission.
+                final int permissionCheck = context.checkCallingPermission(
+                        Manifest.permission.PACKAGE_USAGE_STATS);
+                return permissionCheck == PackageManager.PERMISSION_GRANTED;
+            }
+            return (mode == AppOpsManager.MODE_ALLOWED);
+        }
+        return false;
+    }
+}
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
new file mode 100644
index 0000000..b59a890
--- /dev/null
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -0,0 +1,956 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.ROAMING_YES;
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.UID_REMOVED;
+import static android.text.format.DateUtils.WEEK_IN_MILLIS;
+
+import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.NetworkStats.State;
+import android.net.NetworkStatsHistory.Entry;
+import android.os.Binder;
+import android.service.NetworkStatsCollectionKeyProto;
+import android.service.NetworkStatsCollectionProto;
+import android.service.NetworkStatsCollectionStatsProto;
+import android.telephony.SubscriptionPlan;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.Range;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FileRotator;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.NetworkStatsUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedInputStream;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.ProtocolException;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Collection of {@link NetworkStatsHistory}, stored based on combined key of
+ * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.Writer {
+    private static final String TAG = NetworkStatsCollection.class.getSimpleName();
+    /** File header magic number: "ANET" */
+    private static final int FILE_MAGIC = 0x414E4554;
+
+    private static final int VERSION_NETWORK_INIT = 1;
+
+    private static final int VERSION_UID_INIT = 1;
+    private static final int VERSION_UID_WITH_IDENT = 2;
+    private static final int VERSION_UID_WITH_TAG = 3;
+    private static final int VERSION_UID_WITH_SET = 4;
+
+    private static final int VERSION_UNIFIED_INIT = 16;
+
+    private ArrayMap<Key, NetworkStatsHistory> mStats = new ArrayMap<>();
+
+    private final long mBucketDurationMillis;
+
+    private long mStartMillis;
+    private long mEndMillis;
+    private long mTotalBytes;
+    private boolean mDirty;
+
+    /**
+     * Construct a {@link NetworkStatsCollection} object.
+     *
+     * @param bucketDuration duration of the buckets in this object, in milliseconds.
+     * @hide
+     */
+    public NetworkStatsCollection(long bucketDurationMillis) {
+        mBucketDurationMillis = bucketDurationMillis;
+        reset();
+    }
+
+    /** @hide */
+    public void clear() {
+        reset();
+    }
+
+    /** @hide */
+    public void reset() {
+        mStats.clear();
+        mStartMillis = Long.MAX_VALUE;
+        mEndMillis = Long.MIN_VALUE;
+        mTotalBytes = 0;
+        mDirty = false;
+    }
+
+    /** @hide */
+    public long getStartMillis() {
+        return mStartMillis;
+    }
+
+    /**
+     * Return first atomic bucket in this collection, which is more conservative
+     * than {@link #mStartMillis}.
+     * @hide
+     */
+    public long getFirstAtomicBucketMillis() {
+        if (mStartMillis == Long.MAX_VALUE) {
+            return Long.MAX_VALUE;
+        } else {
+            return mStartMillis + mBucketDurationMillis;
+        }
+    }
+
+    /** @hide */
+    public long getEndMillis() {
+        return mEndMillis;
+    }
+
+    /** @hide */
+    public long getTotalBytes() {
+        return mTotalBytes;
+    }
+
+    /** @hide */
+    public boolean isDirty() {
+        return mDirty;
+    }
+
+    /** @hide */
+    public void clearDirty() {
+        mDirty = false;
+    }
+
+    /** @hide */
+    public boolean isEmpty() {
+        return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE;
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public long roundUp(long time) {
+        if (time == Long.MIN_VALUE || time == Long.MAX_VALUE
+                || time == SubscriptionPlan.TIME_UNKNOWN) {
+            return time;
+        } else {
+            final long mod = time % mBucketDurationMillis;
+            if (mod > 0) {
+                time -= mod;
+                time += mBucketDurationMillis;
+            }
+            return time;
+        }
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public long roundDown(long time) {
+        if (time == Long.MIN_VALUE || time == Long.MAX_VALUE
+                || time == SubscriptionPlan.TIME_UNKNOWN) {
+            return time;
+        } else {
+            final long mod = time % mBucketDurationMillis;
+            if (mod > 0) {
+                time -= mod;
+            }
+            return time;
+        }
+    }
+
+    /** @hide */
+    public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel) {
+        return getRelevantUids(accessLevel, Binder.getCallingUid());
+    }
+
+    /** @hide */
+    public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel,
+                final int callerUid) {
+        final ArrayList<Integer> uids = new ArrayList<>();
+        for (int i = 0; i < mStats.size(); i++) {
+            final Key key = mStats.keyAt(i);
+            if (NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)) {
+                int j = Collections.binarySearch(uids, new Integer(key.uid));
+
+                if (j < 0) {
+                    j = ~j;
+                    uids.add(j, key.uid);
+                }
+            }
+        }
+        return CollectionUtils.toIntArray(uids);
+    }
+
+    /**
+     * Combine all {@link NetworkStatsHistory} in this collection which match
+     * the requested parameters.
+     * @hide
+     */
+    public NetworkStatsHistory getHistory(NetworkTemplate template, SubscriptionPlan augmentPlan,
+            int uid, int set, int tag, int fields, long start, long end,
+            @NetworkStatsAccess.Level int accessLevel, int callerUid) {
+        if (!NetworkStatsAccess.isAccessibleToUser(uid, callerUid, accessLevel)) {
+            throw new SecurityException("Network stats history of uid " + uid
+                    + " is forbidden for caller " + callerUid);
+        }
+
+        // 180 days of history should be enough for anyone; if we end up needing
+        // more, we'll dynamically grow the history object.
+        final int bucketEstimate = (int) NetworkStatsUtils.constrain(
+                ((end - start) / mBucketDurationMillis), 0,
+                (180 * DateUtils.DAY_IN_MILLIS) / mBucketDurationMillis);
+        final NetworkStatsHistory combined = new NetworkStatsHistory(
+                mBucketDurationMillis, bucketEstimate, fields);
+
+        // shortcut when we know stats will be empty
+        if (start == end) return combined;
+
+        // Figure out the window of time that we should be augmenting (if any)
+        long augmentStart = SubscriptionPlan.TIME_UNKNOWN;
+        long augmentEnd = (augmentPlan != null) ? augmentPlan.getDataUsageTime()
+                : SubscriptionPlan.TIME_UNKNOWN;
+        // And if augmenting, we might need to collect more data to adjust with
+        long collectStart = start;
+        long collectEnd = end;
+
+        if (augmentEnd != SubscriptionPlan.TIME_UNKNOWN) {
+            final Iterator<Range<ZonedDateTime>> it = augmentPlan.cycleIterator();
+            while (it.hasNext()) {
+                final Range<ZonedDateTime> cycle = it.next();
+                final long cycleStart = cycle.getLower().toInstant().toEpochMilli();
+                final long cycleEnd = cycle.getUpper().toInstant().toEpochMilli();
+                if (cycleStart <= augmentEnd && augmentEnd < cycleEnd) {
+                    augmentStart = cycleStart;
+                    collectStart = Long.min(collectStart, augmentStart);
+                    collectEnd = Long.max(collectEnd, augmentEnd);
+                    break;
+                }
+            }
+        }
+
+        if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) {
+            // Shrink augmentation window so we don't risk undercounting.
+            augmentStart = roundUp(augmentStart);
+            augmentEnd = roundDown(augmentEnd);
+            // Grow collection window so we get all the stats needed.
+            collectStart = roundDown(collectStart);
+            collectEnd = roundUp(collectEnd);
+        }
+
+        for (int i = 0; i < mStats.size(); i++) {
+            final Key key = mStats.keyAt(i);
+            if (key.uid == uid && NetworkStats.setMatches(set, key.set) && key.tag == tag
+                    && templateMatches(template, key.ident)) {
+                final NetworkStatsHistory value = mStats.valueAt(i);
+                combined.recordHistory(value, collectStart, collectEnd);
+            }
+        }
+
+        if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) {
+            final NetworkStatsHistory.Entry entry = combined.getValues(
+                    augmentStart, augmentEnd, null);
+
+            // If we don't have any recorded data for this time period, give
+            // ourselves something to scale with.
+            if (entry.rxBytes == 0 || entry.txBytes == 0) {
+                combined.recordData(augmentStart, augmentEnd,
+                        new NetworkStats.Entry(1, 0, 1, 0, 0));
+                combined.getValues(augmentStart, augmentEnd, entry);
+            }
+
+            final long rawBytes = (entry.rxBytes + entry.txBytes) == 0 ? 1 :
+                    (entry.rxBytes + entry.txBytes);
+            final long rawRxBytes = entry.rxBytes == 0 ? 1 : entry.rxBytes;
+            final long rawTxBytes = entry.txBytes == 0 ? 1 : entry.txBytes;
+            final long targetBytes = augmentPlan.getDataUsageBytes();
+
+            final long targetRxBytes = multiplySafeByRational(targetBytes, rawRxBytes, rawBytes);
+            final long targetTxBytes = multiplySafeByRational(targetBytes, rawTxBytes, rawBytes);
+
+
+            // Scale all matching buckets to reach anchor target
+            final long beforeTotal = combined.getTotalBytes();
+            for (int i = 0; i < combined.size(); i++) {
+                combined.getValues(i, entry);
+                if (entry.bucketStart >= augmentStart
+                        && entry.bucketStart + entry.bucketDuration <= augmentEnd) {
+                    entry.rxBytes = multiplySafeByRational(
+                            targetRxBytes, entry.rxBytes, rawRxBytes);
+                    entry.txBytes = multiplySafeByRational(
+                            targetTxBytes, entry.txBytes, rawTxBytes);
+                    // We purposefully clear out packet counters to indicate
+                    // that this data has been augmented.
+                    entry.rxPackets = 0;
+                    entry.txPackets = 0;
+                    combined.setValues(i, entry);
+                }
+            }
+
+            final long deltaTotal = combined.getTotalBytes() - beforeTotal;
+            if (deltaTotal != 0) {
+                Log.d(TAG, "Augmented network usage by " + deltaTotal + " bytes");
+            }
+
+            // Finally we can slice data as originally requested
+            final NetworkStatsHistory sliced = new NetworkStatsHistory(
+                    mBucketDurationMillis, bucketEstimate, fields);
+            sliced.recordHistory(combined, start, end);
+            return sliced;
+        } else {
+            return combined;
+        }
+    }
+
+    /**
+     * Summarize all {@link NetworkStatsHistory} in this collection which match
+     * the requested parameters across the requested range.
+     *
+     * @param template - a predicate for filtering netstats.
+     * @param start - start of the range, timestamp in milliseconds since the epoch.
+     * @param end - end of the range, timestamp in milliseconds since the epoch.
+     * @param accessLevel - caller access level.
+     * @param callerUid - caller UID.
+     * @hide
+     */
+    public NetworkStats getSummary(NetworkTemplate template, long start, long end,
+            @NetworkStatsAccess.Level int accessLevel, int callerUid) {
+        final long now = System.currentTimeMillis();
+
+        final NetworkStats stats = new NetworkStats(end - start, 24);
+
+        // shortcut when we know stats will be empty
+        if (start == end) return stats;
+
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        NetworkStatsHistory.Entry historyEntry = null;
+
+        for (int i = 0; i < mStats.size(); i++) {
+            final Key key = mStats.keyAt(i);
+            if (templateMatches(template, key.ident)
+                    && NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)
+                    && key.set < NetworkStats.SET_DEBUG_START) {
+                final NetworkStatsHistory value = mStats.valueAt(i);
+                historyEntry = value.getValues(start, end, now, historyEntry);
+
+                entry.iface = IFACE_ALL;
+                entry.uid = key.uid;
+                entry.set = key.set;
+                entry.tag = key.tag;
+                entry.defaultNetwork = key.ident.areAllMembersOnDefaultNetwork()
+                        ? DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO;
+                entry.metered = key.ident.isAnyMemberMetered() ? METERED_YES : METERED_NO;
+                entry.roaming = key.ident.isAnyMemberRoaming() ? ROAMING_YES : ROAMING_NO;
+                entry.rxBytes = historyEntry.rxBytes;
+                entry.rxPackets = historyEntry.rxPackets;
+                entry.txBytes = historyEntry.txBytes;
+                entry.txPackets = historyEntry.txPackets;
+                entry.operations = historyEntry.operations;
+
+                if (!entry.isEmpty()) {
+                    stats.combineValues(entry);
+                }
+            }
+        }
+
+        return stats;
+    }
+
+    /**
+     * Record given {@link android.net.NetworkStats.Entry} into this collection.
+     * @hide
+     */
+    public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start,
+            long end, NetworkStats.Entry entry) {
+        final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag);
+        history.recordData(start, end, entry);
+        noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes);
+    }
+
+    /**
+     * Record given {@link NetworkStatsHistory} into this collection.
+     *
+     * @hide
+     */
+    public void recordHistory(@NonNull Key key, @NonNull NetworkStatsHistory history) {
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(history);
+        if (history.size() == 0) return;
+        noteRecordedHistory(history.getStart(), history.getEnd(), history.getTotalBytes());
+
+        NetworkStatsHistory target = mStats.get(key);
+        if (target == null) {
+            target = new NetworkStatsHistory(history.getBucketDuration());
+            mStats.put(key, target);
+        }
+        target.recordEntireHistory(history);
+    }
+
+    /**
+     * Record all {@link NetworkStatsHistory} contained in the given collection
+     * into this collection.
+     *
+     * @hide
+     */
+    public void recordCollection(@NonNull NetworkStatsCollection another) {
+        Objects.requireNonNull(another);
+        for (int i = 0; i < another.mStats.size(); i++) {
+            final Key key = another.mStats.keyAt(i);
+            final NetworkStatsHistory value = another.mStats.valueAt(i);
+            recordHistory(key, value);
+        }
+    }
+
+    private NetworkStatsHistory findOrCreateHistory(
+            NetworkIdentitySet ident, int uid, int set, int tag) {
+        final Key key = new Key(ident, uid, set, tag);
+        final NetworkStatsHistory existing = mStats.get(key);
+
+        // update when no existing, or when bucket duration changed
+        NetworkStatsHistory updated = null;
+        if (existing == null) {
+            updated = new NetworkStatsHistory(mBucketDurationMillis, 10);
+        } else if (existing.getBucketDuration() != mBucketDurationMillis) {
+            updated = new NetworkStatsHistory(existing, mBucketDurationMillis);
+        }
+
+        if (updated != null) {
+            mStats.put(key, updated);
+            return updated;
+        } else {
+            return existing;
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void read(InputStream in) throws IOException {
+        read((DataInput) new DataInputStream(in));
+    }
+
+    private void read(DataInput in) throws IOException {
+        // verify file magic header intact
+        final int magic = in.readInt();
+        if (magic != FILE_MAGIC) {
+            throw new ProtocolException("unexpected magic: " + magic);
+        }
+
+        final int version = in.readInt();
+        switch (version) {
+            case VERSION_UNIFIED_INIT: {
+                // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
+                final int identSize = in.readInt();
+                for (int i = 0; i < identSize; i++) {
+                    final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+
+                    final int size = in.readInt();
+                    for (int j = 0; j < size; j++) {
+                        final int uid = in.readInt();
+                        final int set = in.readInt();
+                        final int tag = in.readInt();
+
+                        final Key key = new Key(ident, uid, set, tag);
+                        final NetworkStatsHistory history = new NetworkStatsHistory(in);
+                        recordHistory(key, history);
+                    }
+                }
+                break;
+            }
+            default: {
+                throw new ProtocolException("unexpected version: " + version);
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void write(OutputStream out) throws IOException {
+        write((DataOutput) new DataOutputStream(out));
+        out.flush();
+    }
+
+    private void write(DataOutput out) throws IOException {
+        // cluster key lists grouped by ident
+        final HashMap<NetworkIdentitySet, ArrayList<Key>> keysByIdent = new HashMap<>();
+        for (Key key : mStats.keySet()) {
+            ArrayList<Key> keys = keysByIdent.get(key.ident);
+            if (keys == null) {
+                keys = new ArrayList<>();
+                keysByIdent.put(key.ident, keys);
+            }
+            keys.add(key);
+        }
+
+        out.writeInt(FILE_MAGIC);
+        out.writeInt(VERSION_UNIFIED_INIT);
+
+        out.writeInt(keysByIdent.size());
+        for (NetworkIdentitySet ident : keysByIdent.keySet()) {
+            final ArrayList<Key> keys = keysByIdent.get(ident);
+            ident.writeToStream(out);
+
+            out.writeInt(keys.size());
+            for (Key key : keys) {
+                final NetworkStatsHistory history = mStats.get(key);
+                out.writeInt(key.uid);
+                out.writeInt(key.set);
+                out.writeInt(key.tag);
+                history.writeToStream(out);
+            }
+        }
+    }
+
+    /**
+     * Read legacy network summary statistics file format into the collection,
+     * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
+     *
+     * @deprecated
+     * @hide
+     */
+    @Deprecated
+    public void readLegacyNetwork(File file) throws IOException {
+        final AtomicFile inputFile = new AtomicFile(file);
+
+        DataInputStream in = null;
+        try {
+            in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
+
+            // verify file magic header intact
+            final int magic = in.readInt();
+            if (magic != FILE_MAGIC) {
+                throw new ProtocolException("unexpected magic: " + magic);
+            }
+
+            final int version = in.readInt();
+            switch (version) {
+                case VERSION_NETWORK_INIT: {
+                    // network := size *(NetworkIdentitySet NetworkStatsHistory)
+                    final int size = in.readInt();
+                    for (int i = 0; i < size; i++) {
+                        final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+                        final NetworkStatsHistory history = new NetworkStatsHistory(in);
+
+                        final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE);
+                        recordHistory(key, history);
+                    }
+                    break;
+                }
+                default: {
+                    throw new ProtocolException("unexpected version: " + version);
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // missing stats is okay, probably first boot
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    /**
+     * Read legacy Uid statistics file format into the collection,
+     * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
+     *
+     * @deprecated
+     * @hide
+     */
+    @Deprecated
+    public void readLegacyUid(File file, boolean onlyTags) throws IOException {
+        final AtomicFile inputFile = new AtomicFile(file);
+
+        DataInputStream in = null;
+        try {
+            in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
+
+            // verify file magic header intact
+            final int magic = in.readInt();
+            if (magic != FILE_MAGIC) {
+                throw new ProtocolException("unexpected magic: " + magic);
+            }
+
+            final int version = in.readInt();
+            switch (version) {
+                case VERSION_UID_INIT: {
+                    // uid := size *(UID NetworkStatsHistory)
+
+                    // drop this data version, since we don't have a good
+                    // mapping into NetworkIdentitySet.
+                    break;
+                }
+                case VERSION_UID_WITH_IDENT: {
+                    // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory))
+
+                    // drop this data version, since this version only existed
+                    // for a short time.
+                    break;
+                }
+                case VERSION_UID_WITH_TAG:
+                case VERSION_UID_WITH_SET: {
+                    // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
+                    final int identSize = in.readInt();
+                    for (int i = 0; i < identSize; i++) {
+                        final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+
+                        final int size = in.readInt();
+                        for (int j = 0; j < size; j++) {
+                            final int uid = in.readInt();
+                            final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt()
+                                    : SET_DEFAULT;
+                            final int tag = in.readInt();
+
+                            final Key key = new Key(ident, uid, set, tag);
+                            final NetworkStatsHistory history = new NetworkStatsHistory(in);
+
+                            if ((tag == TAG_NONE) != onlyTags) {
+                                recordHistory(key, history);
+                            }
+                        }
+                    }
+                    break;
+                }
+                default: {
+                    throw new ProtocolException("unexpected version: " + version);
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // missing stats is okay, probably first boot
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    /**
+     * Remove any {@link NetworkStatsHistory} attributed to the requested UID,
+     * moving any {@link NetworkStats#TAG_NONE} series to
+     * {@link TrafficStats#UID_REMOVED}.
+     * @hide
+     */
+    public void removeUids(int[] uids) {
+        final ArrayList<Key> knownKeys = new ArrayList<>();
+        knownKeys.addAll(mStats.keySet());
+
+        // migrate all UID stats into special "removed" bucket
+        for (Key key : knownKeys) {
+            if (CollectionUtils.contains(uids, key.uid)) {
+                // only migrate combined TAG_NONE history
+                if (key.tag == TAG_NONE) {
+                    final NetworkStatsHistory uidHistory = mStats.get(key);
+                    final NetworkStatsHistory removedHistory = findOrCreateHistory(
+                            key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE);
+                    removedHistory.recordEntireHistory(uidHistory);
+                }
+                mStats.remove(key);
+                mDirty = true;
+            }
+        }
+    }
+
+    private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) {
+        if (startMillis < mStartMillis) mStartMillis = startMillis;
+        if (endMillis > mEndMillis) mEndMillis = endMillis;
+        mTotalBytes += totalBytes;
+        mDirty = true;
+    }
+
+    private int estimateBuckets() {
+        return (int) (Math.min(mEndMillis - mStartMillis, WEEK_IN_MILLIS * 5)
+                / mBucketDurationMillis);
+    }
+
+    private ArrayList<Key> getSortedKeys() {
+        final ArrayList<Key> keys = new ArrayList<>();
+        keys.addAll(mStats.keySet());
+        Collections.sort(keys, (left, right) -> Key.compare(left, right));
+        return keys;
+    }
+
+    /** @hide */
+    public void dump(IndentingPrintWriter pw) {
+        for (Key key : getSortedKeys()) {
+            pw.print("ident="); pw.print(key.ident.toString());
+            pw.print(" uid="); pw.print(key.uid);
+            pw.print(" set="); pw.print(NetworkStats.setToString(key.set));
+            pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag));
+
+            final NetworkStatsHistory history = mStats.get(key);
+            pw.increaseIndent();
+            history.dump(pw, true);
+            pw.decreaseIndent();
+        }
+    }
+
+    /** @hide */
+    public void dumpDebug(ProtoOutputStream proto, long tag) {
+        final long start = proto.start(tag);
+
+        for (Key key : getSortedKeys()) {
+            final long startStats = proto.start(NetworkStatsCollectionProto.STATS);
+
+            // Key
+            final long startKey = proto.start(NetworkStatsCollectionStatsProto.KEY);
+            key.ident.dumpDebug(proto, NetworkStatsCollectionKeyProto.IDENTITY);
+            proto.write(NetworkStatsCollectionKeyProto.UID, key.uid);
+            proto.write(NetworkStatsCollectionKeyProto.SET, key.set);
+            proto.write(NetworkStatsCollectionKeyProto.TAG, key.tag);
+            proto.end(startKey);
+
+            // Value
+            final NetworkStatsHistory history = mStats.get(key);
+            history.dumpDebug(proto, NetworkStatsCollectionStatsProto.HISTORY);
+            proto.end(startStats);
+        }
+
+        proto.end(start);
+    }
+
+    /** @hide */
+    public void dumpCheckin(PrintWriter pw, long start, long end) {
+        dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateMobileWildcard(), "cell");
+        dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateWifiWildcard(), "wifi");
+        dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateEthernet(), "eth");
+        dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateBluetooth(), "bt");
+    }
+
+    /**
+     * Dump all contained stats that match requested parameters, but group
+     * together all matching {@link NetworkTemplate} under a single prefix.
+     */
+    private void dumpCheckin(PrintWriter pw, long start, long end, NetworkTemplate groupTemplate,
+            String groupPrefix) {
+        final ArrayMap<Key, NetworkStatsHistory> grouped = new ArrayMap<>();
+
+        // Walk through all history, grouping by matching network templates
+        for (int i = 0; i < mStats.size(); i++) {
+            final Key key = mStats.keyAt(i);
+            final NetworkStatsHistory value = mStats.valueAt(i);
+
+            if (!templateMatches(groupTemplate, key.ident)) continue;
+            if (key.set >= NetworkStats.SET_DEBUG_START) continue;
+
+            final Key groupKey = new Key(new NetworkIdentitySet(), key.uid, key.set, key.tag);
+            NetworkStatsHistory groupHistory = grouped.get(groupKey);
+            if (groupHistory == null) {
+                groupHistory = new NetworkStatsHistory(value.getBucketDuration());
+                grouped.put(groupKey, groupHistory);
+            }
+            groupHistory.recordHistory(value, start, end);
+        }
+
+        for (int i = 0; i < grouped.size(); i++) {
+            final Key key = grouped.keyAt(i);
+            final NetworkStatsHistory value = grouped.valueAt(i);
+
+            if (value.size() == 0) continue;
+
+            pw.print("c,");
+            pw.print(groupPrefix); pw.print(',');
+            pw.print(key.uid); pw.print(',');
+            pw.print(NetworkStats.setToCheckinString(key.set)); pw.print(',');
+            pw.print(key.tag);
+            pw.println();
+
+            value.dumpCheckin(pw);
+        }
+    }
+
+    /**
+     * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity}
+     * in the given {@link NetworkIdentitySet}.
+     */
+    private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) {
+        for (NetworkIdentity ident : identSet) {
+            if (template.matches(ident)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get the all historical stats of the collection {@link NetworkStatsCollection}.
+     *
+     * @return All {@link NetworkStatsHistory} in this collection.
+     */
+    @NonNull
+    public Map<Key, NetworkStatsHistory> getEntries() {
+        return new ArrayMap(mStats);
+    }
+
+    /**
+     * Builder class for {@link NetworkStatsCollection}.
+     */
+    public static final class Builder {
+        private final long mBucketDurationMillis;
+        private final ArrayMap<Key, NetworkStatsHistory> mEntries = new ArrayMap<>();
+
+        /**
+         * Creates a new Builder with given bucket duration.
+         *
+         * @param bucketDuration Duration of the buckets of the object, in milliseconds.
+         */
+        public Builder(long bucketDurationMillis) {
+            mBucketDurationMillis = bucketDurationMillis;
+        }
+
+        /**
+         * Add association of the history with the specified key in this map.
+         *
+         * @param key The object used to identify a network, see {@link Key}.
+         * @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}.
+         * @return The builder object.
+         */
+        @NonNull
+        public NetworkStatsCollection.Builder addEntry(@NonNull Key key,
+                @NonNull NetworkStatsHistory history) {
+            Objects.requireNonNull(key);
+            Objects.requireNonNull(history);
+            final List<Entry> historyEntries = history.getEntries();
+
+            final NetworkStatsHistory.Builder historyBuilder =
+                    new NetworkStatsHistory.Builder(mBucketDurationMillis, historyEntries.size());
+            for (Entry entry : historyEntries) {
+                historyBuilder.addEntry(entry);
+            }
+
+            mEntries.put(key, historyBuilder.build());
+            return this;
+        }
+
+        /**
+         * Builds the instance of the {@link NetworkStatsCollection}.
+         *
+         * @return the built instance of {@link NetworkStatsCollection}.
+         */
+        @NonNull
+        public NetworkStatsCollection build() {
+            final NetworkStatsCollection collection =
+                    new NetworkStatsCollection(mBucketDurationMillis);
+            for (int i = 0; i < mEntries.size(); i++) {
+                collection.recordHistory(mEntries.keyAt(i), mEntries.valueAt(i));
+            }
+            return collection;
+        }
+    }
+
+    /**
+     * the identifier that associate with the {@link NetworkStatsHistory} object to identify
+     * a certain record in the {@link NetworkStatsCollection} object.
+     */
+    public static final class Key {
+        /** @hide */
+        public final NetworkIdentitySet ident;
+        /** @hide */
+        public final int uid;
+        /** @hide */
+        public final int set;
+        /** @hide */
+        public final int tag;
+
+        private final int mHashCode;
+
+        /**
+         * Construct a {@link Key} object.
+         *
+         * @param ident a Set of {@link NetworkIdentity} that associated with the record.
+         * @param uid Uid of the record.
+         * @param set Set of the record, see {@code NetworkStats#SET_*}.
+         * @param tag Tag of the record, see {@link TrafficStats#setThreadStatsTag(int)}.
+         */
+        public Key(@NonNull Set<NetworkIdentity> ident, int uid, @State int set, int tag) {
+            this(new NetworkIdentitySet(Objects.requireNonNull(ident)), uid, set, tag);
+        }
+
+        /** @hide */
+        public Key(@NonNull NetworkIdentitySet ident, int uid, int set, int tag) {
+            this.ident = Objects.requireNonNull(ident);
+            this.uid = uid;
+            this.set = set;
+            this.tag = tag;
+            mHashCode = Objects.hash(ident, uid, set, tag);
+        }
+
+        @Override
+        public int hashCode() {
+            return mHashCode;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            if (obj instanceof Key) {
+                final Key key = (Key) obj;
+                return uid == key.uid && set == key.set && tag == key.tag
+                        && Objects.equals(ident, key.ident);
+            }
+            return false;
+        }
+
+        /** @hide */
+        public static int compare(@NonNull Key left, @NonNull Key right) {
+            Objects.requireNonNull(left);
+            Objects.requireNonNull(right);
+            int res = 0;
+            if (left.ident != null && right.ident != null) {
+                res = NetworkIdentitySet.compare(left.ident, right.ident);
+            }
+            if (res == 0) {
+                res = Integer.compare(left.uid, right.uid);
+            }
+            if (res == 0) {
+                res = Integer.compare(left.set, right.set);
+            }
+            if (res == 0) {
+                res = Integer.compare(left.tag, right.tag);
+            }
+            return res;
+        }
+    }
+}
diff --git a/framework-t/src/android/net/NetworkStatsHistory.aidl b/framework-t/src/android/net/NetworkStatsHistory.aidl
new file mode 100644
index 0000000..8b9069f
--- /dev/null
+++ b/framework-t/src/android/net/NetworkStatsHistory.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable NetworkStatsHistory;
diff --git a/framework-t/src/android/net/NetworkStatsHistory.java b/framework-t/src/android/net/NetworkStatsHistory.java
new file mode 100644
index 0000000..301fef9
--- /dev/null
+++ b/framework-t/src/android/net/NetworkStatsHistory.java
@@ -0,0 +1,1162 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStatsHistory.DataStreamUtils.readFullLongArray;
+import static android.net.NetworkStatsHistory.DataStreamUtils.readVarLongArray;
+import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray;
+import static android.net.NetworkStatsHistory.Entry.UNKNOWN;
+import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray;
+import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.NetworkStatsHistoryBucketProto;
+import android.service.NetworkStatsHistoryProto;
+import android.util.IndentingPrintWriter;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.NetworkStatsUtils;
+
+import libcore.util.EmptyArray;
+
+import java.io.CharArrayWriter;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Collection of historical network statistics, recorded into equally-sized
+ * "buckets" in time. Internally it stores data in {@code long} series for more
+ * efficient persistence.
+ * <p>
+ * Each bucket is defined by a {@link #bucketStart} timestamp, and lasts for
+ * {@link #bucketDuration}. Internally assumes that {@link #bucketStart} is
+ * sorted at all times.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class NetworkStatsHistory implements Parcelable {
+    private static final int VERSION_INIT = 1;
+    private static final int VERSION_ADD_PACKETS = 2;
+    private static final int VERSION_ADD_ACTIVE = 3;
+
+    /** @hide */
+    public static final int FIELD_ACTIVE_TIME = 0x01;
+    /** @hide */
+    public static final int FIELD_RX_BYTES = 0x02;
+    /** @hide */
+    public static final int FIELD_RX_PACKETS = 0x04;
+    /** @hide */
+    public static final int FIELD_TX_BYTES = 0x08;
+    /** @hide */
+    public static final int FIELD_TX_PACKETS = 0x10;
+    /** @hide */
+    public static final int FIELD_OPERATIONS = 0x20;
+    /** @hide */
+    public static final int FIELD_ALL = 0xFFFFFFFF;
+
+    private long bucketDuration;
+    private int bucketCount;
+    private long[] bucketStart;
+    private long[] activeTime;
+    private long[] rxBytes;
+    private long[] rxPackets;
+    private long[] txBytes;
+    private long[] txPackets;
+    private long[] operations;
+    private long totalBytes;
+
+    /** @hide */
+    public NetworkStatsHistory(long bucketDuration, long[] bucketStart, long[] activeTime,
+            long[] rxBytes, long[] rxPackets, long[] txBytes, long[] txPackets,
+            long[] operations, int bucketCount, long totalBytes) {
+        this.bucketDuration = bucketDuration;
+        this.bucketStart = bucketStart;
+        this.activeTime = activeTime;
+        this.rxBytes = rxBytes;
+        this.rxPackets = rxPackets;
+        this.txBytes = txBytes;
+        this.txPackets = txPackets;
+        this.operations = operations;
+        this.bucketCount = bucketCount;
+        this.totalBytes = totalBytes;
+    }
+
+    /**
+     * An instance to represent a single record in a {@link NetworkStatsHistory} object.
+     */
+    public static final class Entry {
+        /** @hide */
+        public static final long UNKNOWN = -1;
+
+        /** @hide */
+        // TODO: Migrate all callers to get duration from the history object and remove this field.
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public long bucketDuration;
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public long bucketStart;
+        /** @hide */
+        public long activeTime;
+        /** @hide */
+        @UnsupportedAppUsage
+        public long rxBytes;
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public long rxPackets;
+        /** @hide */
+        @UnsupportedAppUsage
+        public long txBytes;
+        /** @hide */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public long txPackets;
+        /** @hide */
+        public long operations;
+        /** @hide */
+        Entry() {}
+
+        /**
+         * Construct a {@link Entry} instance to represent a single record in a
+         * {@link NetworkStatsHistory} object.
+         *
+         * @param bucketStart Start of period for this {@link Entry}, in milliseconds since the
+         *                    Unix epoch, see {@link java.lang.System#currentTimeMillis}.
+         * @param activeTime Active time for this {@link Entry}, in milliseconds.
+         * @param rxBytes Number of bytes received for this {@link Entry}. Statistics should
+         *                represent the contents of IP packets, including IP headers.
+         * @param rxPackets Number of packets received for this {@link Entry}. Statistics should
+         *                  represent the contents of IP packets, including IP headers.
+         * @param txBytes Number of bytes transmitted for this {@link Entry}. Statistics should
+         *                represent the contents of IP packets, including IP headers.
+         * @param txPackets Number of bytes transmitted for this {@link Entry}. Statistics should
+         *                  represent the contents of IP packets, including IP headers.
+         * @param operations count of network operations performed for this {@link Entry}. This can
+         *                   be used to derive bytes-per-operation.
+         */
+        public Entry(long bucketStart, long activeTime, long rxBytes,
+                long rxPackets, long txBytes, long txPackets, long operations) {
+            this.bucketStart = bucketStart;
+            this.activeTime = activeTime;
+            this.rxBytes = rxBytes;
+            this.rxPackets = rxPackets;
+            this.txBytes = txBytes;
+            this.txPackets = txPackets;
+            this.operations = operations;
+        }
+
+        /**
+         * Get start timestamp of the bucket's time interval, in milliseconds since the Unix epoch.
+         */
+        public long getBucketStart() {
+            return bucketStart;
+        }
+
+        /**
+         * Get active time of the bucket's time interval, in milliseconds.
+         */
+        public long getActiveTime() {
+            return activeTime;
+        }
+
+        /** Get number of bytes received for this {@link Entry}. */
+        public long getRxBytes() {
+            return rxBytes;
+        }
+
+        /** Get number of packets received for this {@link Entry}. */
+        public long getRxPackets() {
+            return rxPackets;
+        }
+
+        /** Get number of bytes transmitted for this {@link Entry}. */
+        public long getTxBytes() {
+            return txBytes;
+        }
+
+        /** Get number of packets transmitted for this {@link Entry}. */
+        public long getTxPackets() {
+            return txPackets;
+        }
+
+        /** Get count of network operations performed for this {@link Entry}. */
+        public long getOperations() {
+            return operations;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o.getClass() != getClass()) return false;
+            Entry entry = (Entry) o;
+            return bucketStart == entry.bucketStart
+                    && activeTime == entry.activeTime && rxBytes == entry.rxBytes
+                    && rxPackets == entry.rxPackets && txBytes == entry.txBytes
+                    && txPackets == entry.txPackets && operations == entry.operations;
+        }
+
+        @Override
+        public int hashCode() {
+            return (int) (bucketStart * 2
+                    + activeTime * 3
+                    + rxBytes * 5
+                    + rxPackets * 7
+                    + txBytes * 11
+                    + txPackets * 13
+                    + operations * 17);
+        }
+
+        @Override
+        public String toString() {
+            return "Entry{"
+                    + "bucketStart=" + bucketStart
+                    + ", activeTime=" + activeTime
+                    + ", rxBytes=" + rxBytes
+                    + ", rxPackets=" + rxPackets
+                    + ", txBytes=" + txBytes
+                    + ", txPackets=" + txPackets
+                    + ", operations=" + operations
+                    + "}";
+        }
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public NetworkStatsHistory(long bucketDuration) {
+        this(bucketDuration, 10, FIELD_ALL);
+    }
+
+    /** @hide */
+    public NetworkStatsHistory(long bucketDuration, int initialSize) {
+        this(bucketDuration, initialSize, FIELD_ALL);
+    }
+
+    /** @hide */
+    public NetworkStatsHistory(long bucketDuration, int initialSize, int fields) {
+        this.bucketDuration = bucketDuration;
+        bucketStart = new long[initialSize];
+        if ((fields & FIELD_ACTIVE_TIME) != 0) activeTime = new long[initialSize];
+        if ((fields & FIELD_RX_BYTES) != 0) rxBytes = new long[initialSize];
+        if ((fields & FIELD_RX_PACKETS) != 0) rxPackets = new long[initialSize];
+        if ((fields & FIELD_TX_BYTES) != 0) txBytes = new long[initialSize];
+        if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize];
+        if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize];
+        bucketCount = 0;
+        totalBytes = 0;
+    }
+
+    /** @hide */
+    public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) {
+        this(bucketDuration, existing.estimateResizeBuckets(bucketDuration));
+        recordEntireHistory(existing);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public NetworkStatsHistory(Parcel in) {
+        bucketDuration = in.readLong();
+        bucketStart = readLongArray(in);
+        activeTime = readLongArray(in);
+        rxBytes = readLongArray(in);
+        rxPackets = readLongArray(in);
+        txBytes = readLongArray(in);
+        txPackets = readLongArray(in);
+        operations = readLongArray(in);
+        bucketCount = bucketStart.length;
+        totalBytes = in.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeLong(bucketDuration);
+        writeLongArray(out, bucketStart, bucketCount);
+        writeLongArray(out, activeTime, bucketCount);
+        writeLongArray(out, rxBytes, bucketCount);
+        writeLongArray(out, rxPackets, bucketCount);
+        writeLongArray(out, txBytes, bucketCount);
+        writeLongArray(out, txPackets, bucketCount);
+        writeLongArray(out, operations, bucketCount);
+        out.writeLong(totalBytes);
+    }
+
+    /** @hide */
+    public NetworkStatsHistory(DataInput in) throws IOException {
+        final int version = in.readInt();
+        switch (version) {
+            case VERSION_INIT: {
+                bucketDuration = in.readLong();
+                bucketStart = readFullLongArray(in);
+                rxBytes = readFullLongArray(in);
+                rxPackets = new long[bucketStart.length];
+                txBytes = readFullLongArray(in);
+                txPackets = new long[bucketStart.length];
+                operations = new long[bucketStart.length];
+                bucketCount = bucketStart.length;
+                totalBytes = CollectionUtils.total(rxBytes) + CollectionUtils.total(txBytes);
+                break;
+            }
+            case VERSION_ADD_PACKETS:
+            case VERSION_ADD_ACTIVE: {
+                bucketDuration = in.readLong();
+                bucketStart = readVarLongArray(in);
+                activeTime = (version >= VERSION_ADD_ACTIVE) ? readVarLongArray(in)
+                        : new long[bucketStart.length];
+                rxBytes = readVarLongArray(in);
+                rxPackets = readVarLongArray(in);
+                txBytes = readVarLongArray(in);
+                txPackets = readVarLongArray(in);
+                operations = readVarLongArray(in);
+                bucketCount = bucketStart.length;
+                totalBytes = CollectionUtils.total(rxBytes) + CollectionUtils.total(txBytes);
+                break;
+            }
+            default: {
+                throw new ProtocolException("unexpected version: " + version);
+            }
+        }
+
+        if (bucketStart.length != bucketCount || rxBytes.length != bucketCount
+                || rxPackets.length != bucketCount || txBytes.length != bucketCount
+                || txPackets.length != bucketCount || operations.length != bucketCount) {
+            throw new ProtocolException("Mismatched history lengths");
+        }
+    }
+
+    /** @hide */
+    public void writeToStream(DataOutput out) throws IOException {
+        out.writeInt(VERSION_ADD_ACTIVE);
+        out.writeLong(bucketDuration);
+        writeVarLongArray(out, bucketStart, bucketCount);
+        writeVarLongArray(out, activeTime, bucketCount);
+        writeVarLongArray(out, rxBytes, bucketCount);
+        writeVarLongArray(out, rxPackets, bucketCount);
+        writeVarLongArray(out, txBytes, bucketCount);
+        writeVarLongArray(out, txPackets, bucketCount);
+        writeVarLongArray(out, operations, bucketCount);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public int size() {
+        return bucketCount;
+    }
+
+    /** @hide */
+    public long getBucketDuration() {
+        return bucketDuration;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public long getStart() {
+        if (bucketCount > 0) {
+            return bucketStart[0];
+        } else {
+            return Long.MAX_VALUE;
+        }
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public long getEnd() {
+        if (bucketCount > 0) {
+            return bucketStart[bucketCount - 1] + bucketDuration;
+        } else {
+            return Long.MIN_VALUE;
+        }
+    }
+
+    /**
+     * Return total bytes represented by this history.
+     * @hide
+     */
+    public long getTotalBytes() {
+        return totalBytes;
+    }
+
+    /**
+     * Return index of bucket that contains or is immediately before the
+     * requested time.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public int getIndexBefore(long time) {
+        int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time);
+        if (index < 0) {
+            index = (~index) - 1;
+        } else {
+            index -= 1;
+        }
+        return NetworkStatsUtils.constrain(index, 0, bucketCount - 1);
+    }
+
+    /**
+     * Return index of bucket that contains or is immediately after the
+     * requested time.
+     * @hide
+     */
+    public int getIndexAfter(long time) {
+        int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time);
+        if (index < 0) {
+            index = ~index;
+        } else {
+            index += 1;
+        }
+        return NetworkStatsUtils.constrain(index, 0, bucketCount - 1);
+    }
+
+    /**
+     * Return specific stats entry.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public Entry getValues(int i, Entry recycle) {
+        final Entry entry = recycle != null ? recycle : new Entry();
+        entry.bucketStart = bucketStart[i];
+        entry.bucketDuration = bucketDuration;
+        entry.activeTime = getLong(activeTime, i, UNKNOWN);
+        entry.rxBytes = getLong(rxBytes, i, UNKNOWN);
+        entry.rxPackets = getLong(rxPackets, i, UNKNOWN);
+        entry.txBytes = getLong(txBytes, i, UNKNOWN);
+        entry.txPackets = getLong(txPackets, i, UNKNOWN);
+        entry.operations = getLong(operations, i, UNKNOWN);
+        return entry;
+    }
+
+    /**
+     * Get List of {@link Entry} of the {@link NetworkStatsHistory} instance.
+     *
+     * @return
+     */
+    @NonNull
+    public List<Entry> getEntries() {
+        // TODO: Return a wrapper that uses this list instead, to prevent the returned result
+        //  from being changed.
+        final ArrayList<Entry> ret = new ArrayList<>(size());
+        for (int i = 0; i < size(); i++) {
+            ret.add(getValues(i, null /* recycle */));
+        }
+        return ret;
+    }
+
+    /** @hide */
+    public void setValues(int i, Entry entry) {
+        // Unwind old values
+        if (rxBytes != null) totalBytes -= rxBytes[i];
+        if (txBytes != null) totalBytes -= txBytes[i];
+
+        bucketStart[i] = entry.bucketStart;
+        setLong(activeTime, i, entry.activeTime);
+        setLong(rxBytes, i, entry.rxBytes);
+        setLong(rxPackets, i, entry.rxPackets);
+        setLong(txBytes, i, entry.txBytes);
+        setLong(txPackets, i, entry.txPackets);
+        setLong(operations, i, entry.operations);
+
+        // Apply new values
+        if (rxBytes != null) totalBytes += rxBytes[i];
+        if (txBytes != null) totalBytes += txBytes[i];
+    }
+
+    /**
+     * Record that data traffic occurred in the given time range. Will
+     * distribute across internal buckets, creating new buckets as needed.
+     * @hide
+     */
+    @Deprecated
+    public void recordData(long start, long end, long rxBytes, long txBytes) {
+        recordData(start, end, new NetworkStats.Entry(
+                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, 0L, txBytes, 0L, 0L));
+    }
+
+    /**
+     * Record that data traffic occurred in the given time range. Will
+     * distribute across internal buckets, creating new buckets as needed.
+     * @hide
+     */
+    public void recordData(long start, long end, NetworkStats.Entry entry) {
+        long rxBytes = entry.rxBytes;
+        long rxPackets = entry.rxPackets;
+        long txBytes = entry.txBytes;
+        long txPackets = entry.txPackets;
+        long operations = entry.operations;
+
+        if (entry.isNegative()) {
+            throw new IllegalArgumentException("tried recording negative data");
+        }
+        if (entry.isEmpty()) {
+            return;
+        }
+
+        // create any buckets needed by this range
+        ensureBuckets(start, end);
+        // Return fast if there is still no entry. This would typically happen when the start,
+        // end or duration are not valid values, e.g. start > end, negative duration value, etc.
+        if (bucketCount == 0) return;
+
+        // distribute data usage into buckets
+        long duration = end - start;
+        final int startIndex = getIndexAfter(end);
+        for (int i = startIndex; i >= 0; i--) {
+            final long curStart = bucketStart[i];
+            final long curEnd = curStart + bucketDuration;
+
+            // bucket is older than record; we're finished
+            if (curEnd < start) break;
+            // bucket is newer than record; keep looking
+            if (curStart > end) continue;
+
+            final long overlap = Math.min(curEnd, end) - Math.max(curStart, start);
+            if (overlap <= 0) continue;
+
+            // integer math each time is faster than floating point
+            final long fracRxBytes = multiplySafeByRational(rxBytes, overlap, duration);
+            final long fracRxPackets = multiplySafeByRational(rxPackets, overlap, duration);
+            final long fracTxBytes = multiplySafeByRational(txBytes, overlap, duration);
+            final long fracTxPackets = multiplySafeByRational(txPackets, overlap, duration);
+            final long fracOperations = multiplySafeByRational(operations, overlap, duration);
+
+
+            addLong(activeTime, i, overlap);
+            addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes;
+            addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets;
+            addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes;
+            addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets;
+            addLong(this.operations, i, fracOperations); operations -= fracOperations;
+
+            duration -= overlap;
+        }
+
+        totalBytes += entry.rxBytes + entry.txBytes;
+    }
+
+    /**
+     * Record an entire {@link NetworkStatsHistory} into this history. Usually
+     * for combining together stats for external reporting.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void recordEntireHistory(NetworkStatsHistory input) {
+        recordHistory(input, Long.MIN_VALUE, Long.MAX_VALUE);
+    }
+
+    /**
+     * Record given {@link NetworkStatsHistory} into this history, copying only
+     * buckets that atomically occur in the inclusive time range. Doesn't
+     * interpolate across partial buckets.
+     * @hide
+     */
+    public void recordHistory(NetworkStatsHistory input, long start, long end) {
+        final NetworkStats.Entry entry = new NetworkStats.Entry(
+                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
+        for (int i = 0; i < input.bucketCount; i++) {
+            final long bucketStart = input.bucketStart[i];
+            final long bucketEnd = bucketStart + input.bucketDuration;
+
+            // skip when bucket is outside requested range
+            if (bucketStart < start || bucketEnd > end) continue;
+
+            entry.rxBytes = getLong(input.rxBytes, i, 0L);
+            entry.rxPackets = getLong(input.rxPackets, i, 0L);
+            entry.txBytes = getLong(input.txBytes, i, 0L);
+            entry.txPackets = getLong(input.txPackets, i, 0L);
+            entry.operations = getLong(input.operations, i, 0L);
+
+            recordData(bucketStart, bucketEnd, entry);
+        }
+    }
+
+    /**
+     * Ensure that buckets exist for given time range, creating as needed.
+     */
+    private void ensureBuckets(long start, long end) {
+        // normalize incoming range to bucket boundaries
+        start -= start % bucketDuration;
+        end += (bucketDuration - (end % bucketDuration)) % bucketDuration;
+
+        for (long now = start; now < end; now += bucketDuration) {
+            // try finding existing bucket
+            final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now);
+            if (index < 0) {
+                // bucket missing, create and insert
+                insertBucket(~index, now);
+            }
+        }
+    }
+
+    /**
+     * Insert new bucket at requested index and starting time.
+     */
+    private void insertBucket(int index, long start) {
+        // create more buckets when needed
+        if (bucketCount >= bucketStart.length) {
+            final int newLength = Math.max(bucketStart.length, 10) * 3 / 2;
+            bucketStart = Arrays.copyOf(bucketStart, newLength);
+            if (activeTime != null) activeTime = Arrays.copyOf(activeTime, newLength);
+            if (rxBytes != null) rxBytes = Arrays.copyOf(rxBytes, newLength);
+            if (rxPackets != null) rxPackets = Arrays.copyOf(rxPackets, newLength);
+            if (txBytes != null) txBytes = Arrays.copyOf(txBytes, newLength);
+            if (txPackets != null) txPackets = Arrays.copyOf(txPackets, newLength);
+            if (operations != null) operations = Arrays.copyOf(operations, newLength);
+        }
+
+        // create gap when inserting bucket in middle
+        if (index < bucketCount) {
+            final int dstPos = index + 1;
+            final int length = bucketCount - index;
+
+            System.arraycopy(bucketStart, index, bucketStart, dstPos, length);
+            if (activeTime != null) System.arraycopy(activeTime, index, activeTime, dstPos, length);
+            if (rxBytes != null) System.arraycopy(rxBytes, index, rxBytes, dstPos, length);
+            if (rxPackets != null) System.arraycopy(rxPackets, index, rxPackets, dstPos, length);
+            if (txBytes != null) System.arraycopy(txBytes, index, txBytes, dstPos, length);
+            if (txPackets != null) System.arraycopy(txPackets, index, txPackets, dstPos, length);
+            if (operations != null) System.arraycopy(operations, index, operations, dstPos, length);
+        }
+
+        bucketStart[index] = start;
+        setLong(activeTime, index, 0L);
+        setLong(rxBytes, index, 0L);
+        setLong(rxPackets, index, 0L);
+        setLong(txBytes, index, 0L);
+        setLong(txPackets, index, 0L);
+        setLong(operations, index, 0L);
+        bucketCount++;
+    }
+
+    /**
+     * Clear all data stored in this object.
+     * @hide
+     */
+    public void clear() {
+        bucketStart = EmptyArray.LONG;
+        if (activeTime != null) activeTime = EmptyArray.LONG;
+        if (rxBytes != null) rxBytes = EmptyArray.LONG;
+        if (rxPackets != null) rxPackets = EmptyArray.LONG;
+        if (txBytes != null) txBytes = EmptyArray.LONG;
+        if (txPackets != null) txPackets = EmptyArray.LONG;
+        if (operations != null) operations = EmptyArray.LONG;
+        bucketCount = 0;
+        totalBytes = 0;
+    }
+
+    /**
+     * Remove buckets older than requested cutoff.
+     * @hide
+     */
+    public void removeBucketsBefore(long cutoff) {
+        // TODO: Consider use getIndexBefore.
+        int i;
+        for (i = 0; i < bucketCount; i++) {
+            final long curStart = bucketStart[i];
+            final long curEnd = curStart + bucketDuration;
+
+            // cutoff happens before or during this bucket; everything before
+            // this bucket should be removed.
+            if (curEnd > cutoff) break;
+        }
+
+        if (i > 0) {
+            final int length = bucketStart.length;
+            bucketStart = Arrays.copyOfRange(bucketStart, i, length);
+            if (activeTime != null) activeTime = Arrays.copyOfRange(activeTime, i, length);
+            if (rxBytes != null) rxBytes = Arrays.copyOfRange(rxBytes, i, length);
+            if (rxPackets != null) rxPackets = Arrays.copyOfRange(rxPackets, i, length);
+            if (txBytes != null) txBytes = Arrays.copyOfRange(txBytes, i, length);
+            if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length);
+            if (operations != null) operations = Arrays.copyOfRange(operations, i, length);
+            bucketCount -= i;
+
+            totalBytes = 0;
+            if (rxBytes != null) totalBytes += CollectionUtils.total(rxBytes);
+            if (txBytes != null) totalBytes += CollectionUtils.total(txBytes);
+        }
+    }
+
+    /**
+     * Return interpolated data usage across the requested range. Interpolates
+     * across buckets, so values may be rounded slightly.
+     *
+     * <p>If the active bucket is not completed yet, it returns the proportional value of it
+     * based on its duration and the {@code end} param.
+     *
+     * @param start - start of the range, timestamp in milliseconds since the epoch.
+     * @param end - end of the range, timestamp in milliseconds since the epoch.
+     * @param recycle - entry instance for performance, could be null.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public Entry getValues(long start, long end, Entry recycle) {
+        return getValues(start, end, Long.MAX_VALUE, recycle);
+    }
+
+    /**
+     * Return interpolated data usage across the requested range. Interpolates
+     * across buckets, so values may be rounded slightly.
+     *
+     * @param start - start of the range, timestamp in milliseconds since the epoch.
+     * @param end - end of the range, timestamp in milliseconds since the epoch.
+     * @param now - current timestamp in milliseconds since the epoch (wall clock).
+     * @param recycle - entry instance for performance, could be null.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public Entry getValues(long start, long end, long now, Entry recycle) {
+        final Entry entry = recycle != null ? recycle : new Entry();
+        entry.bucketDuration = end - start;
+        entry.bucketStart = start;
+        entry.activeTime = activeTime != null ? 0 : UNKNOWN;
+        entry.rxBytes = rxBytes != null ? 0 : UNKNOWN;
+        entry.rxPackets = rxPackets != null ? 0 : UNKNOWN;
+        entry.txBytes = txBytes != null ? 0 : UNKNOWN;
+        entry.txPackets = txPackets != null ? 0 : UNKNOWN;
+        entry.operations = operations != null ? 0 : UNKNOWN;
+
+        // Return fast if there is no entry.
+        if (bucketCount == 0) return entry;
+
+        final int startIndex = getIndexAfter(end);
+        for (int i = startIndex; i >= 0; i--) {
+            final long curStart = bucketStart[i];
+            long curEnd = curStart + bucketDuration;
+
+            // bucket is older than request; we're finished
+            if (curEnd <= start) break;
+            // bucket is newer than request; keep looking
+            if (curStart >= end) continue;
+
+            // the active bucket is shorter then a normal completed bucket
+            if (curEnd > now) curEnd = now;
+            // usually this is simply bucketDuration
+            final long bucketSpan = curEnd - curStart;
+            // prevent division by zero
+            if (bucketSpan <= 0) continue;
+
+            final long overlapEnd = curEnd < end ? curEnd : end;
+            final long overlapStart = curStart > start ? curStart : start;
+            final long overlap = overlapEnd - overlapStart;
+            if (overlap <= 0) continue;
+
+            // integer math each time is faster than floating point
+            if (activeTime != null) {
+                entry.activeTime += multiplySafeByRational(activeTime[i], overlap, bucketSpan);
+            }
+            if (rxBytes != null) {
+                entry.rxBytes += multiplySafeByRational(rxBytes[i], overlap, bucketSpan);
+            }
+            if (rxPackets != null) {
+                entry.rxPackets += multiplySafeByRational(rxPackets[i], overlap, bucketSpan);
+            }
+            if (txBytes != null) {
+                entry.txBytes += multiplySafeByRational(txBytes[i], overlap, bucketSpan);
+            }
+            if (txPackets != null) {
+                entry.txPackets += multiplySafeByRational(txPackets[i], overlap, bucketSpan);
+            }
+            if (operations != null) {
+                entry.operations += multiplySafeByRational(operations[i], overlap, bucketSpan);
+            }
+        }
+        return entry;
+    }
+
+    /**
+     * @deprecated only for temporary testing
+     * @hide
+     */
+    @Deprecated
+    public void generateRandom(long start, long end, long bytes) {
+        final Random r = new Random();
+
+        final float fractionRx = r.nextFloat();
+        final long rxBytes = (long) (bytes * fractionRx);
+        final long txBytes = (long) (bytes * (1 - fractionRx));
+
+        final long rxPackets = rxBytes / 1024;
+        final long txPackets = txBytes / 1024;
+        final long operations = rxBytes / 2048;
+
+        generateRandom(start, end, rxBytes, rxPackets, txBytes, txPackets, operations, r);
+    }
+
+    /**
+     * @deprecated only for temporary testing
+     * @hide
+     */
+    @Deprecated
+    public void generateRandom(long start, long end, long rxBytes, long rxPackets, long txBytes,
+            long txPackets, long operations, Random r) {
+        ensureBuckets(start, end);
+
+        final NetworkStats.Entry entry = new NetworkStats.Entry(
+                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
+        while (rxBytes > 1024 || rxPackets > 128 || txBytes > 1024 || txPackets > 128
+                || operations > 32) {
+            final long curStart = randomLong(r, start, end);
+            final long curEnd = curStart + randomLong(r, 0, (end - curStart) / 2);
+
+            entry.rxBytes = randomLong(r, 0, rxBytes);
+            entry.rxPackets = randomLong(r, 0, rxPackets);
+            entry.txBytes = randomLong(r, 0, txBytes);
+            entry.txPackets = randomLong(r, 0, txPackets);
+            entry.operations = randomLong(r, 0, operations);
+
+            rxBytes -= entry.rxBytes;
+            rxPackets -= entry.rxPackets;
+            txBytes -= entry.txBytes;
+            txPackets -= entry.txPackets;
+            operations -= entry.operations;
+
+            recordData(curStart, curEnd, entry);
+        }
+    }
+
+    /** @hide */
+    public static long randomLong(Random r, long start, long end) {
+        return (long) (start + (r.nextFloat() * (end - start)));
+    }
+
+    /**
+     * Quickly determine if this history intersects with given window.
+     * @hide
+     */
+    public boolean intersects(long start, long end) {
+        final long dataStart = getStart();
+        final long dataEnd = getEnd();
+        if (start >= dataStart && start <= dataEnd) return true;
+        if (end >= dataStart && end <= dataEnd) return true;
+        if (dataStart >= start && dataStart <= end) return true;
+        if (dataEnd >= start && dataEnd <= end) return true;
+        return false;
+    }
+
+    /** @hide */
+    public void dump(IndentingPrintWriter pw, boolean fullHistory) {
+        pw.print("NetworkStatsHistory: bucketDuration=");
+        pw.println(bucketDuration / SECOND_IN_MILLIS);
+        pw.increaseIndent();
+
+        final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32);
+        if (start > 0) {
+            pw.print("(omitting "); pw.print(start); pw.println(" buckets)");
+        }
+
+        for (int i = start; i < bucketCount; i++) {
+            pw.print("st="); pw.print(bucketStart[i] / SECOND_IN_MILLIS);
+            if (rxBytes != null) { pw.print(" rb="); pw.print(rxBytes[i]); }
+            if (rxPackets != null) { pw.print(" rp="); pw.print(rxPackets[i]); }
+            if (txBytes != null) { pw.print(" tb="); pw.print(txBytes[i]); }
+            if (txPackets != null) { pw.print(" tp="); pw.print(txPackets[i]); }
+            if (operations != null) { pw.print(" op="); pw.print(operations[i]); }
+            pw.println();
+        }
+
+        pw.decreaseIndent();
+    }
+
+    /** @hide */
+    public void dumpCheckin(PrintWriter pw) {
+        pw.print("d,");
+        pw.print(bucketDuration / SECOND_IN_MILLIS);
+        pw.println();
+
+        for (int i = 0; i < bucketCount; i++) {
+            pw.print("b,");
+            pw.print(bucketStart[i] / SECOND_IN_MILLIS); pw.print(',');
+            if (rxBytes != null) { pw.print(rxBytes[i]); } else { pw.print("*"); } pw.print(',');
+            if (rxPackets != null) { pw.print(rxPackets[i]); } else { pw.print("*"); } pw.print(',');
+            if (txBytes != null) { pw.print(txBytes[i]); } else { pw.print("*"); } pw.print(',');
+            if (txPackets != null) { pw.print(txPackets[i]); } else { pw.print("*"); } pw.print(',');
+            if (operations != null) { pw.print(operations[i]); } else { pw.print("*"); }
+            pw.println();
+        }
+    }
+
+    /** @hide */
+    public void dumpDebug(ProtoOutputStream proto, long tag) {
+        final long start = proto.start(tag);
+
+        proto.write(NetworkStatsHistoryProto.BUCKET_DURATION_MS, bucketDuration);
+
+        for (int i = 0; i < bucketCount; i++) {
+            final long startBucket = proto.start(NetworkStatsHistoryProto.BUCKETS);
+
+            proto.write(NetworkStatsHistoryBucketProto.BUCKET_START_MS,
+                    bucketStart[i]);
+            dumpDebug(proto, NetworkStatsHistoryBucketProto.RX_BYTES, rxBytes, i);
+            dumpDebug(proto, NetworkStatsHistoryBucketProto.RX_PACKETS, rxPackets, i);
+            dumpDebug(proto, NetworkStatsHistoryBucketProto.TX_BYTES, txBytes, i);
+            dumpDebug(proto, NetworkStatsHistoryBucketProto.TX_PACKETS, txPackets, i);
+            dumpDebug(proto, NetworkStatsHistoryBucketProto.OPERATIONS, operations, i);
+
+            proto.end(startBucket);
+        }
+
+        proto.end(start);
+    }
+
+    private static void dumpDebug(ProtoOutputStream proto, long tag, long[] array, int index) {
+        if (array != null) {
+            proto.write(tag, array[index]);
+        }
+    }
+
+    @Override
+    public String toString() {
+        final CharArrayWriter writer = new CharArrayWriter();
+        dump(new IndentingPrintWriter(writer, "  "), false);
+        return writer.toString();
+    }
+
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
+        @Override
+        public NetworkStatsHistory createFromParcel(Parcel in) {
+            return new NetworkStatsHistory(in);
+        }
+
+        @Override
+        public NetworkStatsHistory[] newArray(int size) {
+            return new NetworkStatsHistory[size];
+        }
+    };
+
+    private static long getLong(long[] array, int i, long value) {
+        return array != null ? array[i] : value;
+    }
+
+    private static void setLong(long[] array, int i, long value) {
+        if (array != null) array[i] = value;
+    }
+
+    private static void addLong(long[] array, int i, long value) {
+        if (array != null) array[i] += value;
+    }
+
+    /** @hide */
+    public int estimateResizeBuckets(long newBucketDuration) {
+        return (int) (size() * getBucketDuration() / newBucketDuration);
+    }
+
+    /**
+     * Utility methods for interacting with {@link DataInputStream} and
+     * {@link DataOutputStream}, mostly dealing with writing partial arrays.
+     * @hide
+     */
+    public static class DataStreamUtils {
+        @Deprecated
+        public static long[] readFullLongArray(DataInput in) throws IOException {
+            final int size = in.readInt();
+            if (size < 0) throw new ProtocolException("negative array size");
+            final long[] values = new long[size];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = in.readLong();
+            }
+            return values;
+        }
+
+        /**
+         * Read variable-length {@link Long} using protobuf-style approach.
+         */
+        public static long readVarLong(DataInput in) throws IOException {
+            int shift = 0;
+            long result = 0;
+            while (shift < 64) {
+                byte b = in.readByte();
+                result |= (long) (b & 0x7F) << shift;
+                if ((b & 0x80) == 0)
+                    return result;
+                shift += 7;
+            }
+            throw new ProtocolException("malformed long");
+        }
+
+        /**
+         * Write variable-length {@link Long} using protobuf-style approach.
+         */
+        public static void writeVarLong(DataOutput out, long value) throws IOException {
+            while (true) {
+                if ((value & ~0x7FL) == 0) {
+                    out.writeByte((int) value);
+                    return;
+                } else {
+                    out.writeByte(((int) value & 0x7F) | 0x80);
+                    value >>>= 7;
+                }
+            }
+        }
+
+        public static long[] readVarLongArray(DataInput in) throws IOException {
+            final int size = in.readInt();
+            if (size == -1) return null;
+            if (size < 0) throw new ProtocolException("negative array size");
+            final long[] values = new long[size];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = readVarLong(in);
+            }
+            return values;
+        }
+
+        public static void writeVarLongArray(DataOutput out, long[] values, int size)
+                throws IOException {
+            if (values == null) {
+                out.writeInt(-1);
+                return;
+            }
+            if (size > values.length) {
+                throw new IllegalArgumentException("size larger than length");
+            }
+            out.writeInt(size);
+            for (int i = 0; i < size; i++) {
+                writeVarLong(out, values[i]);
+            }
+        }
+    }
+
+    /**
+     * Utility methods for interacting with {@link Parcel} structures, mostly
+     * dealing with writing partial arrays.
+     * @hide
+     */
+    public static class ParcelUtils {
+        public static long[] readLongArray(Parcel in) {
+            final int size = in.readInt();
+            if (size == -1) return null;
+            final long[] values = new long[size];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = in.readLong();
+            }
+            return values;
+        }
+
+        public static void writeLongArray(Parcel out, long[] values, int size) {
+            if (values == null) {
+                out.writeInt(-1);
+                return;
+            }
+            if (size > values.length) {
+                throw new IllegalArgumentException("size larger than length");
+            }
+            out.writeInt(size);
+            for (int i = 0; i < size; i++) {
+                out.writeLong(values[i]);
+            }
+        }
+    }
+
+    /**
+     * Builder class for {@link NetworkStatsHistory}.
+     */
+    public static final class Builder {
+        private final long mBucketDuration;
+        private final List<Long> mBucketStart;
+        private final List<Long> mActiveTime;
+        private final List<Long> mRxBytes;
+        private final List<Long> mRxPackets;
+        private final List<Long> mTxBytes;
+        private final List<Long> mTxPackets;
+        private final List<Long> mOperations;
+
+        /**
+         * Creates a new Builder with given bucket duration and initial capacity to construct
+         * {@link NetworkStatsHistory} objects.
+         *
+         * @param bucketDuration Duration of the buckets of the object, in milliseconds.
+         * @param initialCapacity Estimated number of records.
+         */
+        public Builder(long bucketDuration, int initialCapacity) {
+            mBucketDuration = bucketDuration;
+            mBucketStart = new ArrayList<>(initialCapacity);
+            mActiveTime = new ArrayList<>(initialCapacity);
+            mRxBytes = new ArrayList<>(initialCapacity);
+            mRxPackets = new ArrayList<>(initialCapacity);
+            mTxBytes = new ArrayList<>(initialCapacity);
+            mTxPackets = new ArrayList<>(initialCapacity);
+            mOperations = new ArrayList<>(initialCapacity);
+        }
+
+        /**
+         * Add an {@link Entry} into the {@link NetworkStatsHistory} instance.
+         *
+         * @param entry The target {@link Entry} object.
+         * @return The builder object.
+         */
+        @NonNull
+        public Builder addEntry(@NonNull Entry entry) {
+            mBucketStart.add(entry.bucketStart);
+            mActiveTime.add(entry.activeTime);
+            mRxBytes.add(entry.rxBytes);
+            mRxPackets.add(entry.rxPackets);
+            mTxBytes.add(entry.txBytes);
+            mTxPackets.add(entry.txPackets);
+            mOperations.add(entry.operations);
+            return this;
+        }
+
+        private static long sum(@NonNull List<Long> list) {
+            long sum = 0;
+            for (long entry : list) {
+                sum += entry;
+            }
+            return sum;
+        }
+
+        /**
+         * Builds the instance of the {@link NetworkStatsHistory}.
+         *
+         * @return the built instance of {@link NetworkStatsHistory}.
+         */
+        @NonNull
+        public NetworkStatsHistory build() {
+            return new NetworkStatsHistory(mBucketDuration,
+                    CollectionUtils.toLongArray(mBucketStart),
+                    CollectionUtils.toLongArray(mActiveTime),
+                    CollectionUtils.toLongArray(mRxBytes),
+                    CollectionUtils.toLongArray(mRxPackets),
+                    CollectionUtils.toLongArray(mTxBytes),
+                    CollectionUtils.toLongArray(mTxPackets),
+                    CollectionUtils.toLongArray(mOperations),
+                    mBucketStart.size(),
+                    sum(mRxBytes) + sum(mTxBytes));
+        }
+    }
+}
diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java
new file mode 100644
index 0000000..b82a126
--- /dev/null
+++ b/framework-t/src/android/net/NetworkTemplate.java
@@ -0,0 +1,1120 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_PROXY;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.ConnectivityManager.TYPE_WIMAX;
+import static android.net.NetworkIdentity.OEM_NONE;
+import static android.net.NetworkIdentity.OEM_PAID;
+import static android.net.NetworkIdentity.OEM_PRIVATE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.ROAMING_ALL;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.ROAMING_YES;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.usage.NetworkStatsManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.net.wifi.WifiInfo;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.NetworkIdentityUtils;
+import com.android.net.module.util.NetworkStatsUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Predicate used to match {@link NetworkIdentity}, usually when collecting
+ * statistics. (It should probably have been named {@code NetworkPredicate}.)
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class NetworkTemplate implements Parcelable {
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "MATCH_" }, value = {
+            MATCH_MOBILE,
+            MATCH_WIFI,
+            MATCH_ETHERNET,
+            MATCH_BLUETOOTH,
+            MATCH_PROXY,
+            MATCH_CARRIER,
+    })
+    public @interface TemplateMatchRule{}
+
+    /** Match rule to match cellular networks with given Subscriber Ids. */
+    public static final int MATCH_MOBILE = 1;
+    /** Match rule to match wifi networks. */
+    public static final int MATCH_WIFI = 4;
+    /** Match rule to match ethernet networks. */
+    public static final int MATCH_ETHERNET = 5;
+    /**
+     * Match rule to match all cellular networks.
+     *
+     * @hide
+     */
+    public static final int MATCH_MOBILE_WILDCARD = 6;
+    /**
+     * Match rule to match all wifi networks.
+     *
+     * @hide
+     */
+    public static final int MATCH_WIFI_WILDCARD = 7;
+    /** Match rule to match bluetooth networks. */
+    public static final int MATCH_BLUETOOTH = 8;
+    /**
+     * Match rule to match networks with {@link ConnectivityManager#TYPE_PROXY} as the legacy
+     * network type.
+     */
+    public static final int MATCH_PROXY = 9;
+    /**
+     * Match rule to match all networks with subscriberId inside the template. Some carriers
+     * may offer non-cellular networks like WiFi, which will be matched by this rule.
+     */
+    public static final int MATCH_CARRIER = 10;
+
+    // TODO: Remove this and replace all callers with WIFI_NETWORK_KEY_ALL.
+    /** @hide */
+    public static final String WIFI_NETWORKID_ALL = null;
+
+    /**
+     * Wi-Fi Network Key is never supposed to be null (if it is, it is a bug that
+     * should be fixed), so it's not possible to want to match null vs
+     * non-null. Therefore it's fine to use null as a sentinel for Wifi Network Key.
+     *
+     * @hide
+     */
+    public static final String WIFI_NETWORK_KEY_ALL = WIFI_NETWORKID_ALL;
+
+    /**
+     * Include all network types when filtering. This is meant to merge in with the
+     * {@code TelephonyManager.NETWORK_TYPE_*} constants, and thus needs to stay in sync.
+     */
+    public static final int NETWORK_TYPE_ALL = -1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "OEM_MANAGED_" }, value = {
+            OEM_MANAGED_ALL,
+            OEM_MANAGED_NO,
+            OEM_MANAGED_YES,
+            OEM_MANAGED_PAID,
+            OEM_MANAGED_PRIVATE
+    })
+    public @interface OemManaged{}
+
+    /**
+     * Value to match both OEM managed and unmanaged networks (all networks).
+     */
+    public static final int OEM_MANAGED_ALL = -1;
+    /**
+     * Value to match networks which are not OEM managed.
+     */
+    public static final int OEM_MANAGED_NO = OEM_NONE;
+    /**
+     * Value to match any OEM managed network.
+     */
+    public static final int OEM_MANAGED_YES = -2;
+    /**
+     * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PAID}.
+     */
+    public static final int OEM_MANAGED_PAID = OEM_PAID;
+    /**
+     * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PRIVATE}.
+     */
+    public static final int OEM_MANAGED_PRIVATE = OEM_PRIVATE;
+
+    private static boolean isKnownMatchRule(final int rule) {
+        switch (rule) {
+            case MATCH_MOBILE:
+            case MATCH_WIFI:
+            case MATCH_ETHERNET:
+            case MATCH_MOBILE_WILDCARD:
+            case MATCH_WIFI_WILDCARD:
+            case MATCH_BLUETOOTH:
+            case MATCH_PROXY:
+            case MATCH_CARRIER:
+                return true;
+
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Template to match {@link ConnectivityManager#TYPE_MOBILE} networks with
+     * the given IMSI.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateMobileAll(String subscriberId) {
+        return new NetworkTemplate(MATCH_MOBILE, subscriberId, null);
+    }
+
+    /**
+     * Template to match cellular networks with the given IMSI, {@code ratType} and
+     * {@code metered}. Use {@link #NETWORK_TYPE_ALL} to include all network types when
+     * filtering. See {@code TelephonyManager.NETWORK_TYPE_*}.
+     *
+     * @hide
+     */
+    public static NetworkTemplate buildTemplateMobileWithRatType(@Nullable String subscriberId,
+            int ratType, int metered) {
+        if (TextUtils.isEmpty(subscriberId)) {
+            return new NetworkTemplate(MATCH_MOBILE_WILDCARD, null /* subscriberId */,
+                    null /* matchSubscriberIds */,
+                    new String[0] /* matchWifiNetworkKeys */, metered, ROAMING_ALL,
+                    DEFAULT_NETWORK_ALL, ratType, OEM_MANAGED_ALL,
+                    NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
+        }
+        return new NetworkTemplate(MATCH_MOBILE, subscriberId, new String[] { subscriberId },
+                new String[0] /* matchWifiNetworkKeys */,
+                metered, ROAMING_ALL, DEFAULT_NETWORK_ALL, ratType, OEM_MANAGED_ALL,
+                NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
+    }
+
+    /**
+     * Template to match metered {@link ConnectivityManager#TYPE_MOBILE} networks,
+     * regardless of IMSI.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static NetworkTemplate buildTemplateMobileWildcard() {
+        return new NetworkTemplate(MATCH_MOBILE_WILDCARD, null, null);
+    }
+
+    /**
+     * Template to match all metered {@link ConnectivityManager#TYPE_WIFI} networks,
+     * regardless of key of the wifi network.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateWifiWildcard() {
+        // TODO: Consider replace this with MATCH_WIFI with NETWORK_ID_ALL
+        // and SUBSCRIBER_ID_MATCH_RULE_ALL.
+        return new NetworkTemplate(MATCH_WIFI_WILDCARD, null, null);
+    }
+
+    /** @hide */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateWifi() {
+        return buildTemplateWifiWildcard();
+    }
+
+    /**
+     * Template to match {@link ConnectivityManager#TYPE_WIFI} networks with the
+     * given key of the wifi network.
+     *
+     * @param wifiNetworkKey key of the wifi network. see {@link WifiInfo#getNetworkKey()}
+     *                  to know details about the key.
+     * @hide
+     */
+    public static NetworkTemplate buildTemplateWifi(@NonNull String wifiNetworkKey) {
+        Objects.requireNonNull(wifiNetworkKey);
+        return new NetworkTemplate(MATCH_WIFI, null /* subscriberId */,
+                new String[] { null } /* matchSubscriberIds */,
+                new String[] { wifiNetworkKey }, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_ALL,
+                NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL);
+    }
+
+    /**
+     * Template to match all {@link ConnectivityManager#TYPE_WIFI} networks with the given
+     * key of the wifi network and IMSI.
+     *
+     * Call with {@link #WIFI_NETWORK_KEY_ALL} for {@code wifiNetworkKey} to get result regardless
+     * of key of the wifi network.
+     *
+     * @param wifiNetworkKey key of the wifi network. see {@link WifiInfo#getNetworkKey()}
+     *                  to know details about the key.
+     * @param subscriberId the IMSI associated to this wifi network.
+     *
+     * @hide
+     */
+    public static NetworkTemplate buildTemplateWifi(@Nullable String wifiNetworkKey,
+            @Nullable String subscriberId) {
+        return new NetworkTemplate(MATCH_WIFI, subscriberId, new String[] { subscriberId },
+                wifiNetworkKey != null
+                        ? new String[] { wifiNetworkKey } : new String[0],
+                METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_ALL,
+                NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
+    }
+
+    /**
+     * Template to combine all {@link ConnectivityManager#TYPE_ETHERNET} style
+     * networks together.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateEthernet() {
+        return new NetworkTemplate(MATCH_ETHERNET, null, null);
+    }
+
+    /**
+     * Template to combine all {@link ConnectivityManager#TYPE_BLUETOOTH} style
+     * networks together.
+     *
+     * @hide
+     */
+    public static NetworkTemplate buildTemplateBluetooth() {
+        return new NetworkTemplate(MATCH_BLUETOOTH, null, null);
+    }
+
+    /**
+     * Template to combine all {@link ConnectivityManager#TYPE_PROXY} style
+     * networks together.
+     *
+     * @hide
+     */
+    public static NetworkTemplate buildTemplateProxy() {
+        return new NetworkTemplate(MATCH_PROXY, null, null);
+    }
+
+    /**
+     * Template to match all metered carrier networks with the given IMSI.
+     *
+     * @hide
+     */
+    public static NetworkTemplate buildTemplateCarrierMetered(@NonNull String subscriberId) {
+        Objects.requireNonNull(subscriberId);
+        return new NetworkTemplate(MATCH_CARRIER, subscriberId,
+                new String[] { subscriberId },
+                new String[0] /* matchWifiNetworkKeys */,
+                METERED_YES, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_ALL,
+                NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
+    }
+
+    private final int mMatchRule;
+    private final String mSubscriberId;
+
+    /**
+     * Ugh, templates are designed to target a single subscriber, but we might
+     * need to match several "merged" subscribers. These are the subscribers
+     * that should be considered to match this template.
+     * <p>
+     * Since the merge set is dynamic, it should <em>not</em> be persisted or
+     * used for determining equality.
+     */
+    private final String[] mMatchSubscriberIds;
+
+    @NonNull
+    private final String[] mMatchWifiNetworkKeys;
+
+    // Matches for the NetworkStats constants METERED_*, ROAMING_* and DEFAULT_NETWORK_*.
+    private final int mMetered;
+    private final int mRoaming;
+    private final int mDefaultNetwork;
+    private final int mRatType;
+    /**
+     * The subscriber Id match rule defines how the template should match networks with
+     * specific subscriberId(s). See NetworkTemplate#SUBSCRIBER_ID_MATCH_RULE_* for more detail.
+     */
+    private final int mSubscriberIdMatchRule;
+
+    // Bitfield containing OEM network properties{@code NetworkIdentity#OEM_*}.
+    private final int mOemManaged;
+
+    private static void checkValidSubscriberIdMatchRule(int matchRule, int subscriberIdMatchRule) {
+        switch (matchRule) {
+            case MATCH_MOBILE:
+            case MATCH_CARRIER:
+                // MOBILE and CARRIER templates must always specify a subscriber ID.
+                if (subscriberIdMatchRule == NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL) {
+                    throw new IllegalArgumentException("Invalid SubscriberIdMatchRule "
+                            + "on match rule: " + getMatchRuleName(matchRule));
+                }
+                return;
+            default:
+                return;
+        }
+    }
+
+    /** @hide */
+    // TODO: Deprecate this constructor, mark it @UnsupportedAppUsage(maxTargetSdk = S)
+    @UnsupportedAppUsage
+    public NetworkTemplate(int matchRule, String subscriberId, String wifiNetworkKey) {
+        this(matchRule, subscriberId, new String[] { subscriberId }, wifiNetworkKey);
+    }
+
+    /** @hide */
+    public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
+            String wifiNetworkKey) {
+        // Older versions used to only match MATCH_MOBILE and MATCH_MOBILE_WILDCARD templates
+        // to metered networks. It is now possible to match mobile with any meteredness, but
+        // in order to preserve backward compatibility of @UnsupportedAppUsage methods, this
+        //constructor passes METERED_YES for these types.
+        this(matchRule, subscriberId, matchSubscriberIds,
+                wifiNetworkKey != null ? new String[] { wifiNetworkKey } : new String[0],
+                (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD
+                        || matchRule == MATCH_CARRIER) ? METERED_YES : METERED_ALL,
+                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                OEM_MANAGED_ALL, NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
+    }
+
+    /** @hide */
+    // TODO: Remove it after updating all of the caller.
+    public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
+            String wifiNetworkKey, int metered, int roaming, int defaultNetwork, int ratType,
+            int oemManaged) {
+        this(matchRule, subscriberId, matchSubscriberIds,
+                wifiNetworkKey != null ? new String[] { wifiNetworkKey } : new String[0],
+                metered, roaming, defaultNetwork, ratType, oemManaged,
+                NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
+    }
+
+    /** @hide */
+    public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
+            String[] matchWifiNetworkKeys, int metered, int roaming,
+            int defaultNetwork, int ratType, int oemManaged, int subscriberIdMatchRule) {
+        Objects.requireNonNull(matchWifiNetworkKeys);
+        mMatchRule = matchRule;
+        mSubscriberId = subscriberId;
+        // TODO: Check whether mMatchSubscriberIds = null or mMatchSubscriberIds = {null} when
+        // mSubscriberId is null
+        mMatchSubscriberIds = matchSubscriberIds;
+        mMatchWifiNetworkKeys = matchWifiNetworkKeys;
+        mMetered = metered;
+        mRoaming = roaming;
+        mDefaultNetwork = defaultNetwork;
+        mRatType = ratType;
+        mOemManaged = oemManaged;
+        mSubscriberIdMatchRule = subscriberIdMatchRule;
+        checkValidSubscriberIdMatchRule(matchRule, subscriberIdMatchRule);
+        if (!isKnownMatchRule(matchRule)) {
+            throw new IllegalArgumentException("Unknown network template rule " + matchRule
+                    + " will not match any identity.");
+        }
+    }
+
+    private NetworkTemplate(Parcel in) {
+        mMatchRule = in.readInt();
+        mSubscriberId = in.readString();
+        mMatchSubscriberIds = in.createStringArray();
+        mMatchWifiNetworkKeys = in.createStringArray();
+        mMetered = in.readInt();
+        mRoaming = in.readInt();
+        mDefaultNetwork = in.readInt();
+        mRatType = in.readInt();
+        mOemManaged = in.readInt();
+        mSubscriberIdMatchRule = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mMatchRule);
+        dest.writeString(mSubscriberId);
+        dest.writeStringArray(mMatchSubscriberIds);
+        dest.writeStringArray(mMatchWifiNetworkKeys);
+        dest.writeInt(mMetered);
+        dest.writeInt(mRoaming);
+        dest.writeInt(mDefaultNetwork);
+        dest.writeInt(mRatType);
+        dest.writeInt(mOemManaged);
+        dest.writeInt(mSubscriberIdMatchRule);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder("NetworkTemplate: ");
+        builder.append("matchRule=").append(getMatchRuleName(mMatchRule));
+        if (mSubscriberId != null) {
+            builder.append(", subscriberId=").append(
+                    NetworkIdentityUtils.scrubSubscriberId(mSubscriberId));
+        }
+        if (mMatchSubscriberIds != null) {
+            builder.append(", matchSubscriberIds=").append(
+                    Arrays.toString(NetworkIdentityUtils.scrubSubscriberIds(mMatchSubscriberIds)));
+        }
+        builder.append(", matchWifiNetworkKeys=").append(Arrays.toString(mMatchWifiNetworkKeys));
+        if (mMetered != METERED_ALL) {
+            builder.append(", metered=").append(NetworkStats.meteredToString(mMetered));
+        }
+        if (mRoaming != ROAMING_ALL) {
+            builder.append(", roaming=").append(NetworkStats.roamingToString(mRoaming));
+        }
+        if (mDefaultNetwork != DEFAULT_NETWORK_ALL) {
+            builder.append(", defaultNetwork=").append(NetworkStats.defaultNetworkToString(
+                    mDefaultNetwork));
+        }
+        if (mRatType != NETWORK_TYPE_ALL) {
+            builder.append(", ratType=").append(mRatType);
+        }
+        if (mOemManaged != OEM_MANAGED_ALL) {
+            builder.append(", oemManaged=").append(getOemManagedNames(mOemManaged));
+        }
+        builder.append(", subscriberIdMatchRule=")
+                .append(subscriberIdMatchRuleToString(mSubscriberIdMatchRule));
+        return builder.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mMatchRule, mSubscriberId, Arrays.hashCode(mMatchWifiNetworkKeys),
+                mMetered, mRoaming, mDefaultNetwork, mRatType, mOemManaged, mSubscriberIdMatchRule);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof NetworkTemplate) {
+            final NetworkTemplate other = (NetworkTemplate) obj;
+            return mMatchRule == other.mMatchRule
+                    && Objects.equals(mSubscriberId, other.mSubscriberId)
+                    && mMetered == other.mMetered
+                    && mRoaming == other.mRoaming
+                    && mDefaultNetwork == other.mDefaultNetwork
+                    && mRatType == other.mRatType
+                    && mOemManaged == other.mOemManaged
+                    && mSubscriberIdMatchRule == other.mSubscriberIdMatchRule
+                    && Arrays.equals(mMatchWifiNetworkKeys, other.mMatchWifiNetworkKeys);
+        }
+        return false;
+    }
+
+    private static String subscriberIdMatchRuleToString(int rule) {
+        switch (rule) {
+            case NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT:
+                return "EXACT_MATCH";
+            case NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL:
+                return "ALL";
+            default:
+                return "Unknown rule " + rule;
+        }
+    }
+
+    /** @hide */
+    public boolean isMatchRuleMobile() {
+        switch (mMatchRule) {
+            case MATCH_MOBILE:
+            case MATCH_MOBILE_WILDCARD:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Get match rule of the template. See {@code MATCH_*}.
+     */
+    @UnsupportedAppUsage
+    public int getMatchRule() {
+        // Wildcard rules are not exposed. For external callers, convert wildcard rules to
+        // exposed rules before returning.
+        switch (mMatchRule) {
+            case MATCH_MOBILE_WILDCARD:
+                return MATCH_MOBILE;
+            case MATCH_WIFI_WILDCARD:
+                return MATCH_WIFI;
+            default:
+                return mMatchRule;
+        }
+    }
+
+    /**
+     * Get subscriber Id of the template.
+     * @hide
+     */
+    @Nullable
+    @UnsupportedAppUsage
+    public String getSubscriberId() {
+        return mSubscriberId;
+    }
+
+    /**
+     * Get set of subscriber Ids of the template.
+     */
+    @NonNull
+    public Set<String> getSubscriberIds() {
+        return new ArraySet<>(Arrays.asList(mMatchSubscriberIds));
+    }
+
+    /**
+     * Get the set of Wifi Network Keys of the template.
+     * See {@link WifiInfo#getNetworkKey()}.
+     */
+    @NonNull
+    public Set<String> getWifiNetworkKeys() {
+        return new ArraySet<>(Arrays.asList(mMatchWifiNetworkKeys));
+    }
+
+    /** @hide */
+    // TODO: Remove this and replace all callers with {@link #getWifiNetworkKeys()}.
+    @Nullable
+    public String getNetworkId() {
+        return getWifiNetworkKeys().isEmpty() ? null : getWifiNetworkKeys().iterator().next();
+    }
+
+    /**
+     * Get meteredness filter of the template.
+     */
+    @NetworkStats.Meteredness
+    public int getMeteredness() {
+        return mMetered;
+    }
+
+    /**
+     * Get roaming filter of the template.
+     */
+    @NetworkStats.Roaming
+    public int getRoaming() {
+        return mRoaming;
+    }
+
+    /**
+     * Get the default network status filter of the template.
+     */
+    @NetworkStats.DefaultNetwork
+    public int getDefaultNetworkStatus() {
+        return mDefaultNetwork;
+    }
+
+    /**
+     * Get the Radio Access Technology(RAT) type filter of the template.
+     */
+    public int getRatType() {
+        return mRatType;
+    }
+
+    /**
+     * Get the OEM managed filter of the template. See {@code OEM_MANAGED_*} or
+     * {@code android.net.NetworkIdentity#OEM_*}.
+     */
+    @OemManaged
+    public int getOemManaged() {
+        return mOemManaged;
+    }
+
+    /**
+     * Test if given {@link NetworkIdentity} matches this template.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public boolean matches(@NonNull NetworkIdentity ident) {
+        Objects.requireNonNull(ident);
+        if (!matchesMetered(ident)) return false;
+        if (!matchesRoaming(ident)) return false;
+        if (!matchesDefaultNetwork(ident)) return false;
+        if (!matchesOemNetwork(ident)) return false;
+
+        switch (mMatchRule) {
+            case MATCH_MOBILE:
+                return matchesMobile(ident);
+            case MATCH_WIFI:
+                return matchesWifi(ident);
+            case MATCH_ETHERNET:
+                return matchesEthernet(ident);
+            case MATCH_MOBILE_WILDCARD:
+                return matchesMobileWildcard(ident);
+            case MATCH_WIFI_WILDCARD:
+                return matchesWifiWildcard(ident);
+            case MATCH_BLUETOOTH:
+                return matchesBluetooth(ident);
+            case MATCH_PROXY:
+                return matchesProxy(ident);
+            case MATCH_CARRIER:
+                return matchesCarrier(ident);
+            default:
+                // We have no idea what kind of network template we are, so we
+                // just claim not to match anything.
+                return false;
+        }
+    }
+
+    private boolean matchesMetered(NetworkIdentity ident) {
+        return (mMetered == METERED_ALL)
+            || (mMetered == METERED_YES && ident.mMetered)
+            || (mMetered == METERED_NO && !ident.mMetered);
+    }
+
+    private boolean matchesRoaming(NetworkIdentity ident) {
+        return (mRoaming == ROAMING_ALL)
+            || (mRoaming == ROAMING_YES && ident.mRoaming)
+            || (mRoaming == ROAMING_NO && !ident.mRoaming);
+    }
+
+    private boolean matchesDefaultNetwork(NetworkIdentity ident) {
+        return (mDefaultNetwork == DEFAULT_NETWORK_ALL)
+            || (mDefaultNetwork == DEFAULT_NETWORK_YES && ident.mDefaultNetwork)
+            || (mDefaultNetwork == DEFAULT_NETWORK_NO && !ident.mDefaultNetwork);
+    }
+
+    private boolean matchesOemNetwork(NetworkIdentity ident) {
+        return (mOemManaged == OEM_MANAGED_ALL)
+            || (mOemManaged == OEM_MANAGED_YES
+                    && ident.mOemManaged != OEM_NONE)
+            || (mOemManaged == ident.mOemManaged);
+    }
+
+    private boolean matchesCollapsedRatType(NetworkIdentity ident) {
+        return mRatType == NETWORK_TYPE_ALL
+                || NetworkStatsManager.getCollapsedRatType(mRatType)
+                == NetworkStatsManager.getCollapsedRatType(ident.mRatType);
+    }
+
+    /**
+     * Check if this template matches {@code subscriberId}. Returns true if this
+     * template was created with {@code SUBSCRIBER_ID_MATCH_RULE_ALL}, or with a
+     * {@code mMatchSubscriberIds} array that contains {@code subscriberId}.
+     *
+     * @hide
+     */
+    public boolean matchesSubscriberId(@Nullable String subscriberId) {
+        return mSubscriberIdMatchRule == NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
+                || CollectionUtils.contains(mMatchSubscriberIds, subscriberId);
+    }
+
+    /**
+     * Check if network matches key of the wifi network.
+     * Returns true when the key matches, or when {@code mMatchWifiNetworkKeys} is
+     * empty.
+     *
+     * @param wifiNetworkKey key of the wifi network. see {@link WifiInfo#getNetworkKey()}
+     *                  to know details about the key.
+     */
+    private boolean matchesWifiNetworkKey(@NonNull String wifiNetworkKey) {
+        Objects.requireNonNull(wifiNetworkKey);
+        return CollectionUtils.isEmpty(mMatchWifiNetworkKeys)
+                || CollectionUtils.contains(mMatchWifiNetworkKeys, wifiNetworkKey);
+    }
+
+    /**
+     * Check if mobile network matches IMSI.
+     */
+    private boolean matchesMobile(NetworkIdentity ident) {
+        if (ident.mType == TYPE_WIMAX) {
+            // TODO: consider matching against WiMAX subscriber identity
+            return true;
+        } else {
+            return ident.mType == TYPE_MOBILE && !CollectionUtils.isEmpty(mMatchSubscriberIds)
+                    && CollectionUtils.contains(mMatchSubscriberIds, ident.mSubscriberId)
+                    && matchesCollapsedRatType(ident);
+        }
+    }
+
+    /**
+     * Check if matches Wi-Fi network template.
+     */
+    private boolean matchesWifi(NetworkIdentity ident) {
+        switch (ident.mType) {
+            case TYPE_WIFI:
+                return matchesSubscriberId(ident.mSubscriberId)
+                        && matchesWifiNetworkKey(ident.mWifiNetworkKey);
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Check if matches Ethernet network template.
+     */
+    private boolean matchesEthernet(NetworkIdentity ident) {
+        if (ident.mType == TYPE_ETHERNET) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Check if matches carrier network. The carrier networks means it includes the subscriberId.
+     */
+    private boolean matchesCarrier(NetworkIdentity ident) {
+        return ident.mSubscriberId != null
+                && !CollectionUtils.isEmpty(mMatchSubscriberIds)
+                && CollectionUtils.contains(mMatchSubscriberIds, ident.mSubscriberId);
+    }
+
+    private boolean matchesMobileWildcard(NetworkIdentity ident) {
+        if (ident.mType == TYPE_WIMAX) {
+            return true;
+        } else {
+            return ident.mType == TYPE_MOBILE && matchesCollapsedRatType(ident);
+        }
+    }
+
+    private boolean matchesWifiWildcard(NetworkIdentity ident) {
+        switch (ident.mType) {
+            case TYPE_WIFI:
+            case TYPE_WIFI_P2P:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Check if matches Bluetooth network template.
+     */
+    private boolean matchesBluetooth(NetworkIdentity ident) {
+        if (ident.mType == TYPE_BLUETOOTH) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Check if matches Proxy network template.
+     */
+    private boolean matchesProxy(NetworkIdentity ident) {
+        return ident.mType == TYPE_PROXY;
+    }
+
+    private static String getMatchRuleName(int matchRule) {
+        switch (matchRule) {
+            case MATCH_MOBILE:
+                return "MOBILE";
+            case MATCH_WIFI:
+                return "WIFI";
+            case MATCH_ETHERNET:
+                return "ETHERNET";
+            case MATCH_MOBILE_WILDCARD:
+                return "MOBILE_WILDCARD";
+            case MATCH_WIFI_WILDCARD:
+                return "WIFI_WILDCARD";
+            case MATCH_BLUETOOTH:
+                return "BLUETOOTH";
+            case MATCH_PROXY:
+                return "PROXY";
+            case MATCH_CARRIER:
+                return "CARRIER";
+            default:
+                return "UNKNOWN(" + matchRule + ")";
+        }
+    }
+
+    private static String getOemManagedNames(int oemManaged) {
+        switch (oemManaged) {
+            case OEM_MANAGED_ALL:
+                return "OEM_MANAGED_ALL";
+            case OEM_MANAGED_NO:
+                return "OEM_MANAGED_NO";
+            case OEM_MANAGED_YES:
+                return "OEM_MANAGED_YES";
+            default:
+                return NetworkIdentity.getOemManagedNames(oemManaged);
+        }
+    }
+
+    /**
+     * Examine the given template and normalize it.
+     * We pick the "lowest" merged subscriber as the primary
+     * for key purposes, and expand the template to match all other merged
+     * subscribers.
+     * <p>
+     * For example, given an incoming template matching B, and the currently
+     * active merge set [A,B], we'd return a new template that primarily matches
+     * A, but also matches B.
+     * TODO: remove and use {@link #normalize(NetworkTemplate, List)}.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate normalize(NetworkTemplate template, String[] merged) {
+        return normalize(template, Arrays.<String[]>asList(merged));
+    }
+
+    /**
+     * Examine the given template and normalize it.
+     * We pick the "lowest" merged subscriber as the primary
+     * for key purposes, and expand the template to match all other merged
+     * subscribers.
+     *
+     * There can be multiple merged subscriberIds for multi-SIM devices.
+     *
+     * <p>
+     * For example, given an incoming template matching B, and the currently
+     * active merge set [A,B], we'd return a new template that primarily matches
+     * A, but also matches B.
+     *
+     * @hide
+     */
+    // TODO: @SystemApi when ready.
+    public static NetworkTemplate normalize(NetworkTemplate template, List<String[]> mergedList) {
+        // Now there are several types of network which uses SubscriberId to store network
+        // information. For instances:
+        // The TYPE_WIFI with subscriberId means that it is a merged carrier wifi network.
+        // The TYPE_CARRIER means that the network associate to specific carrier network.
+
+        if (template.mSubscriberId == null) return template;
+
+        for (String[] merged : mergedList) {
+            if (CollectionUtils.contains(merged, template.mSubscriberId)) {
+                // Requested template subscriber is part of the merge group; return
+                // a template that matches all merged subscribers.
+                final String[] matchWifiNetworkKeys = template.mMatchWifiNetworkKeys;
+                return new NetworkTemplate(template.mMatchRule, merged[0], merged,
+                        CollectionUtils.isEmpty(matchWifiNetworkKeys)
+                                ? null : matchWifiNetworkKeys[0]);
+            }
+        }
+
+        return template;
+    }
+
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<NetworkTemplate> CREATOR = new Creator<NetworkTemplate>() {
+        @Override
+        public NetworkTemplate createFromParcel(Parcel in) {
+            return new NetworkTemplate(in);
+        }
+
+        @Override
+        public NetworkTemplate[] newArray(int size) {
+            return new NetworkTemplate[size];
+        }
+    };
+
+    /**
+     * Builder class for NetworkTemplate.
+     */
+    public static final class Builder {
+        private final int mMatchRule;
+        // Use a SortedSet to provide a deterministic order when fetching the first one.
+        @NonNull
+        private final SortedSet<String> mMatchSubscriberIds =
+                new TreeSet<>(Comparator.nullsFirst(Comparator.naturalOrder()));
+        @NonNull
+        private final SortedSet<String> mMatchWifiNetworkKeys = new TreeSet<>();
+
+        // Matches for the NetworkStats constants METERED_*, ROAMING_* and DEFAULT_NETWORK_*.
+        private int mMetered;
+        private int mRoaming;
+        private int mDefaultNetwork;
+        private int mRatType;
+
+        // Bitfield containing OEM network properties {@code NetworkIdentity#OEM_*}.
+        private int mOemManaged;
+
+        /**
+         * Creates a new Builder with given match rule to construct NetworkTemplate objects.
+         *
+         * @param matchRule the match rule of the template, see {@code MATCH_*}.
+         */
+        public Builder(@TemplateMatchRule final int matchRule) {
+            assertRequestableMatchRule(matchRule);
+            // Initialize members with default values.
+            mMatchRule = matchRule;
+            mMetered = METERED_ALL;
+            mRoaming = ROAMING_ALL;
+            mDefaultNetwork = DEFAULT_NETWORK_ALL;
+            mRatType = NETWORK_TYPE_ALL;
+            mOemManaged = OEM_MANAGED_ALL;
+        }
+
+        /**
+         * Set the Subscriber Ids. Calling this function with an empty set represents
+         * the intention of matching any Subscriber Ids.
+         *
+         * @param subscriberIds the list of Subscriber Ids.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setSubscriberIds(@NonNull Set<String> subscriberIds) {
+            Objects.requireNonNull(subscriberIds);
+            mMatchSubscriberIds.clear();
+            mMatchSubscriberIds.addAll(subscriberIds);
+            return this;
+        }
+
+        /**
+         * Set the Wifi Network Keys. Calling this function with an empty set represents
+         * the intention of matching any Wifi Network Key.
+         *
+         * @param wifiNetworkKeys the list of Wifi Network Key,
+         *                        see {@link WifiInfo#getNetworkKey()}.
+         *                        Or an empty list to match all networks.
+         *                        Note that {@code getNetworkKey()} might get null key
+         *                        when wifi disconnects. However, the caller should never invoke
+         *                        this function with a null Wifi Network Key since such statistics
+         *                        never exists.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setWifiNetworkKeys(@NonNull Set<String> wifiNetworkKeys) {
+            Objects.requireNonNull(wifiNetworkKeys);
+            for (String key : wifiNetworkKeys) {
+                if (key == null) {
+                    throw new IllegalArgumentException("Null is not a valid key");
+                }
+            }
+            mMatchWifiNetworkKeys.clear();
+            mMatchWifiNetworkKeys.addAll(wifiNetworkKeys);
+            return this;
+        }
+
+        /**
+         * Set the meteredness filter.
+         *
+         * @param metered the meteredness filter.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setMeteredness(@NetworkStats.Meteredness int metered) {
+            mMetered = metered;
+            return this;
+        }
+
+        /**
+         * Set the roaming filter.
+         *
+         * @param roaming the roaming filter.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setRoaming(@NetworkStats.Roaming int roaming) {
+            mRoaming = roaming;
+            return this;
+        }
+
+        /**
+         * Set the default network status filter.
+         *
+         * @param defaultNetwork the default network status filter.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setDefaultNetworkStatus(@NetworkStats.DefaultNetwork int defaultNetwork) {
+            mDefaultNetwork = defaultNetwork;
+            return this;
+        }
+
+        /**
+         * Set the Radio Access Technology(RAT) type filter.
+         *
+         * @param ratType the Radio Access Technology(RAT) type filter. Use
+         *                {@link #NETWORK_TYPE_ALL} to include all network types when filtering.
+         *                See {@code TelephonyManager.NETWORK_TYPE_*}.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setRatType(int ratType) {
+            // Input will be validated with the match rule when building the template.
+            mRatType = ratType;
+            return this;
+        }
+
+        /**
+         * Set the OEM managed filter.
+         *
+         * @param oemManaged the match rule to match different type of OEM managed network or
+         *                   unmanaged networks. See {@code OEM_MANAGED_*}.
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setOemManaged(@OemManaged int oemManaged) {
+            mOemManaged = oemManaged;
+            return this;
+        }
+
+        /**
+         * Check whether the match rule is requestable.
+         *
+         * @param matchRule the target match rule to be checked.
+         */
+        private static void assertRequestableMatchRule(final int matchRule) {
+            if (!isKnownMatchRule(matchRule)
+                    || matchRule == MATCH_PROXY
+                    || matchRule == MATCH_MOBILE_WILDCARD
+                    || matchRule == MATCH_WIFI_WILDCARD) {
+                throw new IllegalArgumentException("Invalid match rule: "
+                        + getMatchRuleName(matchRule));
+            }
+        }
+
+        private void assertRequestableParameters() {
+            validateWifiNetworkKeys();
+            // TODO: Check all the input are legitimate.
+        }
+
+        private void validateWifiNetworkKeys() {
+            if (mMatchRule != MATCH_WIFI && !mMatchWifiNetworkKeys.isEmpty()) {
+                throw new IllegalArgumentException("Trying to build non wifi match rule: "
+                        + mMatchRule + " with wifi network keys");
+            }
+        }
+
+        /**
+         * For backward compatibility, deduce match rule to a wildcard match rule
+         * if the Subscriber Ids are empty.
+         */
+        private int getWildcardDeducedMatchRule() {
+            if (mMatchRule == MATCH_MOBILE && mMatchSubscriberIds.isEmpty()) {
+                return MATCH_MOBILE_WILDCARD;
+            } else if (mMatchRule == MATCH_WIFI && mMatchSubscriberIds.isEmpty()
+                    && mMatchWifiNetworkKeys.isEmpty()) {
+                return MATCH_WIFI_WILDCARD;
+            }
+            return mMatchRule;
+        }
+
+        /**
+         * Builds the instance of the NetworkTemplate.
+         *
+         * @return the built instance of NetworkTemplate.
+         */
+        @NonNull
+        public NetworkTemplate build() {
+            assertRequestableParameters();
+            final int subscriberIdMatchRule = mMatchSubscriberIds.isEmpty()
+                    ? NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
+                    : NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT;
+            return new NetworkTemplate(getWildcardDeducedMatchRule(),
+                    mMatchSubscriberIds.isEmpty() ? null : mMatchSubscriberIds.iterator().next(),
+                    mMatchSubscriberIds.toArray(new String[0]),
+                    mMatchWifiNetworkKeys.toArray(new String[0]), mMetered, mRoaming,
+                    mDefaultNetwork, mRatType, mOemManaged, subscriberIdMatchRule);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
new file mode 100644
index 0000000..dc4ac55
--- /dev/null
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -0,0 +1,1148 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.DownloadManager;
+import android.app.backup.BackupManager;
+import android.app.usage.NetworkStatsManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.os.Binder;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.Socket;
+import java.net.SocketException;
+
+/**
+ * Class that provides network traffic statistics. These statistics include
+ * bytes transmitted and received and network packets transmitted and received,
+ * over all interfaces, over the mobile interface, and on a per-UID basis.
+ * <p>
+ * These statistics may not be available on all platforms. If the statistics are
+ * not supported by this device, {@link #UNSUPPORTED} will be returned.
+ * <p>
+ * Note that the statistics returned by this class reset and start from zero
+ * after every reboot. To access more robust historical network statistics data,
+ * use {@link NetworkStatsManager} instead.
+ */
+public class TrafficStats {
+    static {
+        System.loadLibrary("framework-connectivity-tiramisu-jni");
+    }
+
+    private static final String TAG = TrafficStats.class.getSimpleName();
+    /**
+     * The return value to indicate that the device does not support the statistic.
+     */
+    public final static int UNSUPPORTED = -1;
+
+    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long KB_IN_BYTES = 1024;
+    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
+    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
+    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long TB_IN_BYTES = GB_IN_BYTES * 1024;
+    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long PB_IN_BYTES = TB_IN_BYTES * 1024;
+
+    /**
+     * Special UID value used when collecting {@link NetworkStatsHistory} for
+     * removed applications.
+     *
+     * @hide
+     */
+    public static final int UID_REMOVED = -4;
+
+    /**
+     * Special UID value used when collecting {@link NetworkStatsHistory} for
+     * tethering traffic.
+     *
+     * @hide
+     */
+    public static final int UID_TETHERING = NetworkStats.UID_TETHERING;
+
+    /**
+     * Tag values in this range are reserved for the network stack. The network stack is
+     * running as UID {@link android.os.Process.NETWORK_STACK_UID} when in the mainline
+     * module separate process, and as the system UID otherwise.
+     */
+    /** @hide */
+    @SystemApi
+    public static final int TAG_NETWORK_STACK_RANGE_START = 0xFFFFFD00;
+    /** @hide */
+    @SystemApi
+    public static final int TAG_NETWORK_STACK_RANGE_END = 0xFFFFFEFF;
+
+    /**
+     * Tags between 0xFFFFFF00 and 0xFFFFFFFF are reserved and used internally by system services
+     * like DownloadManager when performing traffic on behalf of an application.
+     */
+    // Please note there is no enforcement of these constants, so do not rely on them to
+    // determine that the caller is a system caller.
+    /** @hide */
+    @SystemApi
+    public static final int TAG_SYSTEM_IMPERSONATION_RANGE_START = 0xFFFFFF00;
+    /** @hide */
+    @SystemApi
+    public static final int TAG_SYSTEM_IMPERSONATION_RANGE_END = 0xFFFFFF0F;
+
+    /**
+     * Tag values between these ranges are reserved for the network stack to do traffic
+     * on behalf of applications. It is a subrange of the range above.
+     */
+    /** @hide */
+    @SystemApi
+    public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_START = 0xFFFFFF80;
+    /** @hide */
+    @SystemApi
+    public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_END = 0xFFFFFF8F;
+
+    /**
+     * Default tag value for {@link DownloadManager} traffic.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_DOWNLOAD = 0xFFFFFF01;
+
+    /**
+     * Default tag value for {@link MediaPlayer} traffic.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_MEDIA = 0xFFFFFF02;
+
+    /**
+     * Default tag value for {@link BackupManager} backup traffic; that is,
+     * traffic from the device to the storage backend.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_BACKUP = 0xFFFFFF03;
+
+    /**
+     * Default tag value for {@link BackupManager} restore traffic; that is,
+     * app data retrieved from the storage backend at install time.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_RESTORE = 0xFFFFFF04;
+
+    /**
+     * Default tag value for code (typically APKs) downloaded by an app store on
+     * behalf of the app, such as updates.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_APP = 0xFFFFFF05;
+
+    // TODO : remove this constant when Wifi code is updated
+    /** @hide */
+    public static final int TAG_SYSTEM_PROBE = 0xFFFFFF42;
+
+    private static INetworkStatsService sStatsService;
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    private synchronized static INetworkStatsService getStatsService() {
+        if (sStatsService == null) {
+            throw new IllegalStateException("TrafficStats not initialized, uid="
+                    + Binder.getCallingUid());
+        }
+        return sStatsService;
+    }
+
+    /**
+     * Snapshot of {@link NetworkStats} when the currently active profiling
+     * session started, or {@code null} if no session active.
+     *
+     * @see #startDataProfiling(Context)
+     * @see #stopDataProfiling(Context)
+     */
+    private static NetworkStats sActiveProfilingStart;
+
+    private static Object sProfilingLock = new Object();
+
+    private static final String LOOPBACK_IFACE = "lo";
+
+    /**
+     * Initialization {@link TrafficStats} with the context, to
+     * allow {@link TrafficStats} to fetch the needed binder.
+     *
+     * @param context a long-lived context, such as the application context or system
+     *                server context.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @SuppressLint("VisiblySynchronized")
+    public static synchronized void init(@NonNull final Context context) {
+        if (sStatsService != null) {
+            throw new IllegalStateException("TrafficStats is already initialized, uid="
+                    + Binder.getCallingUid());
+        }
+        final NetworkStatsManager statsManager =
+                context.getSystemService(NetworkStatsManager.class);
+        if (statsManager == null) {
+            // TODO: Currently Process.isSupplemental is not working yet, because it depends on
+            //  process to run in a certain UID range, which is not true for now. Change this
+            //  to Log.wtf once Process.isSupplemental is ready.
+            Log.e(TAG, "TrafficStats not initialized, uid=" + Binder.getCallingUid());
+            return;
+        }
+        sStatsService = statsManager.getBinder();
+    }
+
+    /**
+     * Attach the socket tagger implementation to the current process, to
+     * get notified when a socket's {@link FileDescriptor} is assigned to
+     * a thread. See {@link SocketTagger#set(SocketTagger)}.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static void attachSocketTagger() {
+        dalvik.system.SocketTagger.set(new SocketTagger());
+    }
+
+    private static class SocketTagger extends dalvik.system.SocketTagger {
+
+        // TODO: set to false
+        private static final boolean LOGD = true;
+
+        SocketTagger() {
+        }
+
+        @Override
+        public void tag(FileDescriptor fd) throws SocketException {
+            final UidTag tagInfo = sThreadUidTag.get();
+            if (LOGD) {
+                Log.d(TAG, "tagSocket(" + fd.getInt$() + ") with statsTag=0x"
+                        + Integer.toHexString(tagInfo.tag) + ", statsUid=" + tagInfo.uid);
+            }
+            if (tagInfo.tag == -1) {
+                StrictMode.noteUntaggedSocket();
+            }
+
+            if (tagInfo.tag == -1 && tagInfo.uid == -1) return;
+            final int errno = native_tagSocketFd(fd, tagInfo.tag, tagInfo.uid);
+            if (errno < 0) {
+                Log.i(TAG, "tagSocketFd(" + fd.getInt$() + ", "
+                        + tagInfo.tag + ", "
+                        + tagInfo.uid + ") failed with errno" + errno);
+            }
+        }
+
+        @Override
+        public void untag(FileDescriptor fd) throws SocketException {
+            if (LOGD) {
+                Log.i(TAG, "untagSocket(" + fd.getInt$() + ")");
+            }
+
+            final UidTag tagInfo = sThreadUidTag.get();
+            if (tagInfo.tag == -1 && tagInfo.uid == -1) return;
+
+            final int errno = native_untagSocketFd(fd);
+            if (errno < 0) {
+                Log.w(TAG, "untagSocket(" + fd.getInt$() + ") failed with errno " + errno);
+            }
+        }
+    }
+
+    private static native int native_tagSocketFd(FileDescriptor fd, int tag, int uid);
+    private static native int native_untagSocketFd(FileDescriptor fd);
+
+    private static class UidTag {
+        public int tag = -1;
+        public int uid = -1;
+    }
+
+    private static ThreadLocal<UidTag> sThreadUidTag = new ThreadLocal<UidTag>() {
+        @Override
+        protected UidTag initialValue() {
+            return new UidTag();
+        }
+    };
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. Only one active tag per thread is supported.
+     * <p>
+     * Changes only take effect during subsequent calls to
+     * {@link #tagSocket(Socket)}.
+     * <p>
+     * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and
+     * used internally by system services like {@link DownloadManager} when
+     * performing traffic on behalf of an application.
+     *
+     * @see #clearThreadStatsTag()
+     */
+    public static void setThreadStatsTag(int tag) {
+        getAndSetThreadStatsTag(tag);
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. Only one active tag per thread is supported.
+     * <p>
+     * Changes only take effect during subsequent calls to
+     * {@link #tagSocket(Socket)}.
+     * <p>
+     * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and
+     * used internally by system services like {@link DownloadManager} when
+     * performing traffic on behalf of an application.
+     *
+     * @return the current tag for the calling thread, which can be used to
+     *         restore any existing values after a nested operation is finished
+     */
+    public static int getAndSetThreadStatsTag(int tag) {
+        final int old = sThreadUidTag.get().tag;
+        sThreadUidTag.get().tag = tag;
+        return old;
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all backup-related traffic.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void setThreadStatsTagBackup() {
+        setThreadStatsTag(TAG_SYSTEM_BACKUP);
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all restore-related traffic.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void setThreadStatsTagRestore() {
+        setThreadStatsTag(TAG_SYSTEM_RESTORE);
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all code (typically APKs) downloaded by an app store on
+     * behalf of the app, such as updates.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void setThreadStatsTagApp() {
+        setThreadStatsTag(TAG_SYSTEM_APP);
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all download provider traffic.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static void setThreadStatsTagDownload() {
+        setThreadStatsTag(TAG_SYSTEM_DOWNLOAD);
+    }
+
+    /**
+     * Get the active tag used when accounting {@link Socket} traffic originating
+     * from the current thread. Only one active tag per thread is supported.
+     * {@link #tagSocket(Socket)}.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static int getThreadStatsTag() {
+        return sThreadUidTag.get().tag;
+    }
+
+    /**
+     * Clear any active tag set to account {@link Socket} traffic originating
+     * from the current thread.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static void clearThreadStatsTag() {
+        sThreadUidTag.get().tag = -1;
+    }
+
+    /**
+     * Set specific UID to use when accounting {@link Socket} traffic
+     * originating from the current thread. Designed for use when performing an
+     * operation on behalf of another application, or when another application
+     * is performing operations on your behalf.
+     * <p>
+     * Any app can <em>accept</em> blame for traffic performed on a socket
+     * originally created by another app by calling this method with the
+     * {@link android.system.Os#getuid()} value. However, only apps holding the
+     * {@code android.Manifest.permission#UPDATE_DEVICE_STATS} permission may
+     * <em>assign</em> blame to another UIDs.
+     * <p>
+     * Changes only take effect during subsequent calls to
+     * {@link #tagSocket(Socket)}.
+     */
+    @SuppressLint("RequiresPermission")
+    public static void setThreadStatsUid(int uid) {
+        sThreadUidTag.get().uid = uid;
+    }
+
+    /**
+     * Get the active UID used when accounting {@link Socket} traffic originating
+     * from the current thread. Only one active tag per thread is supported.
+     * {@link #tagSocket(Socket)}.
+     *
+     * @see #setThreadStatsUid(int)
+     */
+    public static int getThreadStatsUid() {
+        return sThreadUidTag.get().uid;
+    }
+
+    /**
+     * Set specific UID to use when accounting {@link Socket} traffic
+     * originating from the current thread as the calling UID. Designed for use
+     * when another application is performing operations on your behalf.
+     * <p>
+     * Changes only take effect during subsequent calls to
+     * {@link #tagSocket(Socket)}.
+     *
+     * @removed
+     * @deprecated use {@link #setThreadStatsUid(int)} instead.
+     */
+    @Deprecated
+    public static void setThreadStatsUidSelf() {
+        setThreadStatsUid(android.os.Process.myUid());
+    }
+
+    /**
+     * Clear any active UID set to account {@link Socket} traffic originating
+     * from the current thread.
+     *
+     * @see #setThreadStatsUid(int)
+     */
+    @SuppressLint("RequiresPermission")
+    public static void clearThreadStatsUid() {
+        setThreadStatsUid(-1);
+    }
+
+    /**
+     * Tag the given {@link Socket} with any statistics parameters active for
+     * the current thread. Subsequent calls always replace any existing
+     * parameters. When finished, call {@link #untagSocket(Socket)} to remove
+     * statistics parameters.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static void tagSocket(@NonNull Socket socket) throws SocketException {
+        SocketTagger.get().tag(socket);
+    }
+
+    /**
+     * Remove any statistics parameters from the given {@link Socket}.
+     * <p>
+     * In Android 8.1 (API level 27) and lower, a socket is automatically
+     * untagged when it's sent to another process using binder IPC with a
+     * {@code ParcelFileDescriptor} container. In Android 9.0 (API level 28)
+     * and higher, the socket tag is kept when the socket is sent to another
+     * process using binder IPC. You can mimic the previous behavior by
+     * calling {@code untagSocket()} before sending the socket to another
+     * process.
+     */
+    public static void untagSocket(@NonNull Socket socket) throws SocketException {
+        SocketTagger.get().untag(socket);
+    }
+
+    /**
+     * Tag the given {@link DatagramSocket} with any statistics parameters
+     * active for the current thread. Subsequent calls always replace any
+     * existing parameters. When finished, call
+     * {@link #untagDatagramSocket(DatagramSocket)} to remove statistics
+     * parameters.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static void tagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException {
+        SocketTagger.get().tag(socket);
+    }
+
+    /**
+     * Remove any statistics parameters from the given {@link DatagramSocket}.
+     */
+    public static void untagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException {
+        SocketTagger.get().untag(socket);
+    }
+
+    /**
+     * Tag the given {@link FileDescriptor} socket with any statistics
+     * parameters active for the current thread. Subsequent calls always replace
+     * any existing parameters. When finished, call
+     * {@link #untagFileDescriptor(FileDescriptor)} to remove statistics
+     * parameters.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static void tagFileDescriptor(@NonNull FileDescriptor fd) throws IOException {
+        SocketTagger.get().tag(fd);
+    }
+
+    /**
+     * Remove any statistics parameters from the given {@link FileDescriptor}
+     * socket.
+     */
+    public static void untagFileDescriptor(@NonNull FileDescriptor fd) throws IOException {
+        SocketTagger.get().untag(fd);
+    }
+
+    /**
+     * Start profiling data usage for current UID. Only one profiling session
+     * can be active at a time.
+     *
+     * @hide
+     */
+    public static void startDataProfiling(Context context) {
+        synchronized (sProfilingLock) {
+            if (sActiveProfilingStart != null) {
+                throw new IllegalStateException("already profiling data");
+            }
+
+            // take snapshot in time; we calculate delta later
+            sActiveProfilingStart = getDataLayerSnapshotForUid(context);
+        }
+    }
+
+    /**
+     * Stop profiling data usage for current UID.
+     *
+     * @return Detailed {@link NetworkStats} of data that occurred since last
+     *         {@link #startDataProfiling(Context)} call.
+     * @hide
+     */
+    public static NetworkStats stopDataProfiling(Context context) {
+        synchronized (sProfilingLock) {
+            if (sActiveProfilingStart == null) {
+                throw new IllegalStateException("not profiling data");
+            }
+
+            // subtract starting values and return delta
+            final NetworkStats profilingStop = getDataLayerSnapshotForUid(context);
+            final NetworkStats profilingDelta = NetworkStats.subtract(
+                    profilingStop, sActiveProfilingStart, null, null);
+            sActiveProfilingStart = null;
+            return profilingDelta;
+        }
+    }
+
+    /**
+     * Increment count of network operations performed under the accounting tag
+     * currently active on the calling thread. This can be used to derive
+     * bytes-per-operation.
+     *
+     * @param operationCount Number of operations to increment count by.
+     */
+    public static void incrementOperationCount(int operationCount) {
+        final int tag = getThreadStatsTag();
+        incrementOperationCount(tag, operationCount);
+    }
+
+    /**
+     * Increment count of network operations performed under the given
+     * accounting tag. This can be used to derive bytes-per-operation.
+     *
+     * @param tag Accounting tag used in {@link #setThreadStatsTag(int)}.
+     * @param operationCount Number of operations to increment count by.
+     */
+    public static void incrementOperationCount(int tag, int operationCount) {
+        final int uid = android.os.Process.myUid();
+        try {
+            getStatsService().incrementOperationCount(uid, tag, operationCount);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    public static void closeQuietly(INetworkStatsSession session) {
+        // TODO: move to NetworkStatsService once it exists
+        if (session != null) {
+            try {
+                session.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    private static long addIfSupported(long stat) {
+        return (stat == UNSUPPORTED) ? 0 : stat;
+    }
+
+    /**
+     * Return number of packets transmitted across mobile networks since device
+     * boot. Counts packets across all mobile network interfaces, and always
+     * increases monotonically since device boot. Statistics are measured at the
+     * network layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getMobileTxPackets() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            total += addIfSupported(getTxPackets(iface));
+        }
+        return total;
+    }
+
+    /**
+     * Return number of packets received across mobile networks since device
+     * boot. Counts packets across all mobile network interfaces, and always
+     * increases monotonically since device boot. Statistics are measured at the
+     * network layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getMobileRxPackets() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            total += addIfSupported(getRxPackets(iface));
+        }
+        return total;
+    }
+
+    /**
+     * Return number of bytes transmitted across mobile networks since device
+     * boot. Counts packets across all mobile network interfaces, and always
+     * increases monotonically since device boot. Statistics are measured at the
+     * network layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getMobileTxBytes() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            total += addIfSupported(getTxBytes(iface));
+        }
+        return total;
+    }
+
+    /**
+     * Return number of bytes received across mobile networks since device boot.
+     * Counts packets across all mobile network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getMobileRxBytes() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            total += addIfSupported(getRxBytes(iface));
+        }
+        return total;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static long getMobileTcpRxPackets() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            long stat = UNSUPPORTED;
+            try {
+                stat = getStatsService().getIfaceStats(iface, TYPE_TCP_RX_PACKETS);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            total += addIfSupported(stat);
+        }
+        return total;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static long getMobileTcpTxPackets() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            long stat = UNSUPPORTED;
+            try {
+                stat = getStatsService().getIfaceStats(iface, TYPE_TCP_TX_PACKETS);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            total += addIfSupported(stat);
+        }
+        return total;
+    }
+
+    /**
+     * Return the number of packets transmitted on the specified interface since the interface
+     * was created. Statistics are measured at the network layer, so both TCP and
+     * UDP usage are included.
+     *
+     * Note that the returned values are partial statistics that do not count data from several
+     * sources and do not apply several adjustments that are necessary for correctness, such
+     * as adjusting for VPN apps, IPv6-in-IPv4 translation, etc. These values can be used to
+     * determine whether traffic is being transferred on the specific interface but are not a
+     * substitute for the more accurate statistics provided by the {@link NetworkStatsManager}
+     * APIs.
+     *
+     * @param iface The name of the interface.
+     * @return The number of transmitted packets.
+     */
+    public static long getTxPackets(@NonNull String iface) {
+        try {
+            return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return the number of packets received on the specified interface since the interface was
+     * created. Statistics are measured at the network layer, so both TCP
+     * and UDP usage are included.
+     *
+     * Note that the returned values are partial statistics that do not count data from several
+     * sources and do not apply several adjustments that are necessary for correctness, such
+     * as adjusting for VPN apps, IPv6-in-IPv4 translation, etc. These values can be used to
+     * determine whether traffic is being transferred on the specific interface but are not a
+     * substitute for the more accurate statistics provided by the {@link NetworkStatsManager}
+     * APIs.
+     *
+     * @param iface The name of the interface.
+     * @return The number of received packets.
+     */
+    public static long getRxPackets(@NonNull String iface) {
+        try {
+            return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return the number of bytes transmitted on the specified interface since the interface
+     * was created. Statistics are measured at the network layer, so both TCP and
+     * UDP usage are included.
+     *
+     * Note that the returned values are partial statistics that do not count data from several
+     * sources and do not apply several adjustments that are necessary for correctness, such
+     * as adjusting for VPN apps, IPv6-in-IPv4 translation, etc. These values can be used to
+     * determine whether traffic is being transferred on the specific interface but are not a
+     * substitute for the more accurate statistics provided by the {@link NetworkStatsManager}
+     * APIs.
+     *
+     * @param iface The name of the interface.
+     * @return The number of transmitted bytes.
+     */
+    public static long getTxBytes(@NonNull String iface) {
+        try {
+            return getStatsService().getIfaceStats(iface, TYPE_TX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return the number of bytes received on the specified interface since the interface
+     * was created. Statistics are measured at the network layer, so both TCP
+     * and UDP usage are included.
+     *
+     * Note that the returned values are partial statistics that do not count data from several
+     * sources and do not apply several adjustments that are necessary for correctness, such
+     * as adjusting for VPN apps, IPv6-in-IPv4 translation, etc. These values can be used to
+     * determine whether traffic is being transferred on the specific interface but are not a
+     * substitute for the more accurate statistics provided by the {@link NetworkStatsManager}
+     * APIs.
+     *
+     * @param iface The name of the interface.
+     * @return The number of received bytes.
+     */
+    public static long getRxBytes(@NonNull String iface) {
+        try {
+            return getStatsService().getIfaceStats(iface, TYPE_RX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @TestApi
+    public static long getLoopbackTxPackets() {
+        try {
+            return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @TestApi
+    public static long getLoopbackRxPackets() {
+        try {
+            return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @TestApi
+    public static long getLoopbackTxBytes() {
+        try {
+            return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @TestApi
+    public static long getLoopbackRxBytes() {
+        try {
+            return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of packets transmitted since device boot. Counts packets
+     * across all network interfaces, and always increases monotonically since
+     * device boot. Statistics are measured at the network layer, so they
+     * include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getTotalTxPackets() {
+        try {
+            return getStatsService().getTotalStats(TYPE_TX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of packets received since device boot. Counts packets
+     * across all network interfaces, and always increases monotonically since
+     * device boot. Statistics are measured at the network layer, so they
+     * include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getTotalRxPackets() {
+        try {
+            return getStatsService().getTotalStats(TYPE_RX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of bytes transmitted since device boot. Counts packets
+     * across all network interfaces, and always increases monotonically since
+     * device boot. Statistics are measured at the network layer, so they
+     * include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getTotalTxBytes() {
+        try {
+            return getStatsService().getTotalStats(TYPE_TX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of bytes received since device boot. Counts packets across
+     * all network interfaces, and always increases monotonically since device
+     * boot. Statistics are measured at the network layer, so they include both
+     * TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getTotalRxBytes() {
+        try {
+            return getStatsService().getTotalStats(TYPE_RX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of bytes transmitted by the given UID since device boot.
+     * Counts packets across all network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
+     * report traffic statistics for the calling UID. It will return
+     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
+     * historical network statistics belonging to other UIDs, use
+     * {@link NetworkStatsManager}.
+     *
+     * @see android.os.Process#myUid()
+     * @see android.content.pm.ApplicationInfo#uid
+     */
+    public static long getUidTxBytes(int uid) {
+        try {
+            return getStatsService().getUidStats(uid, TYPE_TX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of bytes received by the given UID since device boot.
+     * Counts packets across all network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return
+     * {@link #UNSUPPORTED} on devices where statistics aren't available.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
+     * report traffic statistics for the calling UID. It will return
+     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
+     * historical network statistics belonging to other UIDs, use
+     * {@link NetworkStatsManager}.
+     *
+     * @see android.os.Process#myUid()
+     * @see android.content.pm.ApplicationInfo#uid
+     */
+    public static long getUidRxBytes(int uid) {
+        try {
+            return getStatsService().getUidStats(uid, TYPE_RX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of packets transmitted by the given UID since device boot.
+     * Counts packets across all network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return
+     * {@link #UNSUPPORTED} on devices where statistics aren't available.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
+     * report traffic statistics for the calling UID. It will return
+     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
+     * historical network statistics belonging to other UIDs, use
+     * {@link NetworkStatsManager}.
+     *
+     * @see android.os.Process#myUid()
+     * @see android.content.pm.ApplicationInfo#uid
+     */
+    public static long getUidTxPackets(int uid) {
+        try {
+            return getStatsService().getUidStats(uid, TYPE_TX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of packets received by the given UID since device boot.
+     * Counts packets across all network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return
+     * {@link #UNSUPPORTED} on devices where statistics aren't available.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
+     * report traffic statistics for the calling UID. It will return
+     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
+     * historical network statistics belonging to other UIDs, use
+     * {@link NetworkStatsManager}.
+     *
+     * @see android.os.Process#myUid()
+     * @see android.content.pm.ApplicationInfo#uid
+     */
+    public static long getUidRxPackets(int uid) {
+        try {
+            return getStatsService().getUidStats(uid, TYPE_RX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidTxBytes(int)
+     */
+    @Deprecated
+    public static long getUidTcpTxBytes(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidRxBytes(int)
+     */
+    @Deprecated
+    public static long getUidTcpRxBytes(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidTxBytes(int)
+     */
+    @Deprecated
+    public static long getUidUdpTxBytes(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidRxBytes(int)
+     */
+    @Deprecated
+    public static long getUidUdpRxBytes(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidTxPackets(int)
+     */
+    @Deprecated
+    public static long getUidTcpTxSegments(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidRxPackets(int)
+     */
+    @Deprecated
+    public static long getUidTcpRxSegments(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidTxPackets(int)
+     */
+    @Deprecated
+    public static long getUidUdpTxPackets(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidRxPackets(int)
+     */
+    @Deprecated
+    public static long getUidUdpRxPackets(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * Return detailed {@link NetworkStats} for the current UID. Requires no
+     * special permission.
+     */
+    private static NetworkStats getDataLayerSnapshotForUid(Context context) {
+        // TODO: take snapshot locally, since proc file is now visible
+        final int uid = android.os.Process.myUid();
+        try {
+            return getStatsService().getDataLayerSnapshotForUid(uid);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return set of any ifaces associated with mobile networks since boot.
+     * Interfaces are never removed from this list, so counters should always be
+     * monotonic.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    private static String[] getMobileIfaces() {
+        try {
+            return getStatsService().getMobileIfaces();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    // NOTE: keep these in sync with {@code com_android_server_net_NetworkStatsService.cpp}.
+    /** {@hide} */
+    public static final int TYPE_RX_BYTES = 0;
+    /** {@hide} */
+    public static final int TYPE_RX_PACKETS = 1;
+    /** {@hide} */
+    public static final int TYPE_TX_BYTES = 2;
+    /** {@hide} */
+    public static final int TYPE_TX_PACKETS = 3;
+    /** {@hide} */
+    public static final int TYPE_TCP_RX_PACKETS = 4;
+    /** {@hide} */
+    public static final int TYPE_TCP_TX_PACKETS = 5;
+}
diff --git a/framework-t/src/android/net/UnderlyingNetworkInfo.aidl b/framework-t/src/android/net/UnderlyingNetworkInfo.aidl
new file mode 100644
index 0000000..a56f2f4
--- /dev/null
+++ b/framework-t/src/android/net/UnderlyingNetworkInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable UnderlyingNetworkInfo;
diff --git a/framework-t/src/android/net/UnderlyingNetworkInfo.java b/framework-t/src/android/net/UnderlyingNetworkInfo.java
new file mode 100644
index 0000000..33f9375
--- /dev/null
+++ b/framework-t/src/android/net/UnderlyingNetworkInfo.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A lightweight container used to carry information on the networks that underly a given
+ * virtual network.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class UnderlyingNetworkInfo implements Parcelable {
+    /** The owner of this network. */
+    private final int mOwnerUid;
+
+    /** The interface name of this network. */
+    @NonNull
+    private final String mIface;
+
+    /** The names of the interfaces underlying this network. */
+    @NonNull
+    private final List<String> mUnderlyingIfaces;
+
+    public UnderlyingNetworkInfo(int ownerUid, @NonNull String iface,
+            @NonNull List<String> underlyingIfaces) {
+        Objects.requireNonNull(iface);
+        Objects.requireNonNull(underlyingIfaces);
+        mOwnerUid = ownerUid;
+        mIface = iface;
+        mUnderlyingIfaces = Collections.unmodifiableList(new ArrayList<>(underlyingIfaces));
+    }
+
+    private UnderlyingNetworkInfo(@NonNull Parcel in) {
+        mOwnerUid = in.readInt();
+        mIface = in.readString();
+        List<String> underlyingIfaces = new ArrayList<>();
+        in.readList(underlyingIfaces, null /*classLoader*/);
+        mUnderlyingIfaces = Collections.unmodifiableList(underlyingIfaces);
+    }
+
+    /** Get the owner of this network. */
+    public int getOwnerUid() {
+        return mOwnerUid;
+    }
+
+    /** Get the interface name of this network. */
+    @NonNull
+    public String getInterface() {
+        return mIface;
+    }
+
+    /** Get the names of the interfaces underlying this network. */
+    @NonNull
+    public List<String> getUnderlyingInterfaces() {
+        return mUnderlyingIfaces;
+    }
+
+    @Override
+    public String toString() {
+        return "UnderlyingNetworkInfo{"
+                + "ownerUid=" + mOwnerUid
+                + ", iface='" + mIface + '\''
+                + ", underlyingIfaces='" + mUnderlyingIfaces.toString() + '\''
+                + '}';
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mOwnerUid);
+        dest.writeString(mIface);
+        dest.writeList(mUnderlyingIfaces);
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<UnderlyingNetworkInfo> CREATOR =
+            new Parcelable.Creator<UnderlyingNetworkInfo>() {
+        @NonNull
+        @Override
+        public UnderlyingNetworkInfo createFromParcel(@NonNull Parcel in) {
+            return new UnderlyingNetworkInfo(in);
+        }
+
+        @NonNull
+        @Override
+        public UnderlyingNetworkInfo[] newArray(int size) {
+            return new UnderlyingNetworkInfo[size];
+        }
+    };
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof UnderlyingNetworkInfo)) return false;
+        final UnderlyingNetworkInfo that = (UnderlyingNetworkInfo) o;
+        return mOwnerUid == that.getOwnerUid()
+                && Objects.equals(mIface, that.getInterface())
+                && Objects.equals(mUnderlyingIfaces, that.getUnderlyingInterfaces());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mOwnerUid, mIface, mUnderlyingIfaces);
+    }
+}
diff --git a/framework-t/src/android/net/netstats/IUsageCallback.aidl b/framework-t/src/android/net/netstats/IUsageCallback.aidl
new file mode 100644
index 0000000..4e8a5b2
--- /dev/null
+++ b/framework-t/src/android/net/netstats/IUsageCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netstats;
+
+import android.net.DataUsageRequest;
+
+/**
+ * Interface for NetworkStatsService to notify events to the callers of registerUsageCallback.
+ *
+ * @hide
+ */
+oneway interface IUsageCallback {
+    void onThresholdReached(in DataUsageRequest request);
+    void onCallbackReleased(in DataUsageRequest request);
+}
diff --git a/framework-t/src/android/net/netstats/provider/INetworkStatsProvider.aidl b/framework-t/src/android/net/netstats/provider/INetworkStatsProvider.aidl
new file mode 100644
index 0000000..74c3ba4
--- /dev/null
+++ b/framework-t/src/android/net/netstats/provider/INetworkStatsProvider.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netstats.provider;
+
+/**
+ * Interface for NetworkStatsService to query network statistics and set data limits.
+ *
+ * @hide
+ */
+oneway interface INetworkStatsProvider {
+    void onRequestStatsUpdate(int token);
+    void onSetAlert(long quotaBytes);
+    void onSetWarningAndLimit(String iface, long warningBytes, long limitBytes);
+}
diff --git a/framework-t/src/android/net/netstats/provider/INetworkStatsProviderCallback.aidl b/framework-t/src/android/net/netstats/provider/INetworkStatsProviderCallback.aidl
new file mode 100644
index 0000000..01ff02d
--- /dev/null
+++ b/framework-t/src/android/net/netstats/provider/INetworkStatsProviderCallback.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netstats.provider;
+
+import android.net.NetworkStats;
+
+/**
+ * Interface for implementor of {@link INetworkStatsProviderCallback} to push events
+ * such as network statistics update or notify limit reached.
+ * @hide
+ */
+oneway interface INetworkStatsProviderCallback {
+    void notifyStatsUpdated(int token, in NetworkStats ifaceStats, in NetworkStats uidStats);
+    void notifyAlertReached();
+    void notifyWarningReached();
+    void notifyLimitReached();
+    void unregister();
+}
diff --git a/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java b/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java
new file mode 100644
index 0000000..d37a53d
--- /dev/null
+++ b/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netstats.provider;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.NetworkStats;
+import android.os.RemoteException;
+
+/**
+ * A base class that allows external modules to implement a custom network statistics provider.
+ * @hide
+ */
+@SystemApi
+public abstract class NetworkStatsProvider {
+    /**
+     * A value used by {@link #onSetLimit}, {@link #onSetAlert} and {@link #onSetWarningAndLimit}
+     * indicates there is no limit.
+     */
+    public static final int QUOTA_UNLIMITED = -1;
+
+    @NonNull private final INetworkStatsProvider mProviderBinder =
+            new INetworkStatsProvider.Stub() {
+
+        @Override
+        public void onRequestStatsUpdate(int token) {
+            NetworkStatsProvider.this.onRequestStatsUpdate(token);
+        }
+
+        @Override
+        public void onSetAlert(long quotaBytes) {
+            NetworkStatsProvider.this.onSetAlert(quotaBytes);
+        }
+
+        @Override
+        public void onSetWarningAndLimit(String iface, long warningBytes, long limitBytes) {
+            NetworkStatsProvider.this.onSetWarningAndLimit(iface, warningBytes, limitBytes);
+        }
+    };
+
+    // The binder given by the service when successfully registering. Only null before registering,
+    // never null once non-null.
+    @Nullable
+    private INetworkStatsProviderCallback mProviderCbBinder;
+
+    /**
+     * Return the binder invoked by the service and redirect function calls to the overridden
+     * methods.
+     * @hide
+     */
+    @NonNull
+    public INetworkStatsProvider getProviderBinder() {
+        return mProviderBinder;
+    }
+
+    /**
+     * Store the binder that was returned by the service when successfully registering. Note that
+     * the provider cannot be re-registered. Hence this method can only be called once per provider.
+     *
+     * @hide
+     */
+    public void setProviderCallbackBinder(@NonNull INetworkStatsProviderCallback binder) {
+        if (mProviderCbBinder != null) {
+            throw new IllegalArgumentException("provider is already registered");
+        }
+        mProviderCbBinder = binder;
+    }
+
+    /**
+     * Get the binder that was returned by the service when successfully registering. Or null if the
+     * provider was never registered.
+     *
+     * @hide
+     */
+    @Nullable
+    public INetworkStatsProviderCallback getProviderCallbackBinder() {
+        return mProviderCbBinder;
+    }
+
+    /**
+     * Get the binder that was returned by the service when successfully registering. Throw an
+     * {@link IllegalStateException} if the provider is not registered.
+     *
+     * @hide
+     */
+    @NonNull
+    public INetworkStatsProviderCallback getProviderCallbackBinderOrThrow() {
+        if (mProviderCbBinder == null) {
+            throw new IllegalStateException("the provider is not registered");
+        }
+        return mProviderCbBinder;
+    }
+
+    /**
+     * Notify the system of new network statistics.
+     *
+     * Send the network statistics recorded since the last call to {@link #notifyStatsUpdated}. Must
+     * be called as soon as possible after {@link NetworkStatsProvider#onRequestStatsUpdate(int)}
+     * being called. Responding later increases the probability stats will be dropped. The
+     * provider can also call this whenever it wants to reports new stats for any reason.
+     * Note that the system will not necessarily immediately propagate the statistics to
+     * reflect the update.
+     *
+     * @param token the token under which these stats were gathered. Providers can call this method
+     *              with the current token as often as they want, until the token changes.
+     *              {@see NetworkStatsProvider#onRequestStatsUpdate()}
+     * @param ifaceStats the {@link NetworkStats} per interface to be reported.
+     *                   The provider should not include any traffic that is already counted by
+     *                   kernel interface counters.
+     * @param uidStats the same stats as above, but counts {@link NetworkStats}
+     *                 per uid.
+     */
+    public void notifyStatsUpdated(int token, @NonNull NetworkStats ifaceStats,
+            @NonNull NetworkStats uidStats) {
+        try {
+            getProviderCallbackBinderOrThrow().notifyStatsUpdated(token, ifaceStats, uidStats);
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Notify system that the quota set by {@code onSetAlert} has been reached.
+     */
+    public void notifyAlertReached() {
+        try {
+            getProviderCallbackBinderOrThrow().notifyAlertReached();
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Notify system that the warning set by {@link #onSetWarningAndLimit} has been reached.
+     */
+    public void notifyWarningReached() {
+        try {
+            // Reuse the code path to notify warning reached with limit reached
+            // since framework handles them in the same way.
+            getProviderCallbackBinderOrThrow().notifyWarningReached();
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Notify system that the limit set by {@link #onSetLimit} or limit set by
+     * {@link #onSetWarningAndLimit} has been reached.
+     */
+    public void notifyLimitReached() {
+        try {
+            getProviderCallbackBinderOrThrow().notifyLimitReached();
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Called by {@code NetworkStatsService} when it requires to know updated stats.
+     * The provider MUST respond by calling {@link #notifyStatsUpdated} as soon as possible.
+     * Responding later increases the probability stats will be dropped. Memory allowing, the
+     * system will try to take stats into account up to one minute after calling
+     * {@link #onRequestStatsUpdate}.
+     *
+     * @param token a positive number identifying the new state of the system under which
+     *              {@link NetworkStats} have to be gathered from now on. When this is called,
+     *              custom implementations of providers MUST tally and report the latest stats with
+     *              the previous token, under which stats were being gathered so far.
+     */
+    public abstract void onRequestStatsUpdate(int token);
+
+    /**
+     * Called by {@code NetworkStatsService} when setting the interface quota for the specified
+     * upstream interface. When this is called, the custom implementation should block all egress
+     * packets on the {@code iface} associated with the provider when {@code quotaBytes} bytes have
+     * been reached, and MUST respond to it by calling
+     * {@link NetworkStatsProvider#notifyLimitReached()}.
+     *
+     * @param iface the interface requiring the operation.
+     * @param quotaBytes the quota defined as the number of bytes, starting from zero and counting
+     *                   from now. A value of {@link #QUOTA_UNLIMITED} indicates there is no limit.
+     */
+    public abstract void onSetLimit(@NonNull String iface, long quotaBytes);
+
+    /**
+     * Called by {@code NetworkStatsService} when setting the interface quotas for the specified
+     * upstream interface. If a provider implements {@link #onSetWarningAndLimit}, the system
+     * will not call {@link #onSetLimit}. When this method is called, the implementation
+     * should behave as follows:
+     *   1. If {@code warningBytes} is reached on {@code iface}, block all further traffic on
+     *      {@code iface} and call {@link NetworkStatsProvider@notifyWarningReached()}.
+     *   2. If {@code limitBytes} is reached on {@code iface}, block all further traffic on
+     *   {@code iface} and call {@link NetworkStatsProvider#notifyLimitReached()}.
+     *
+     * @param iface the interface requiring the operation.
+     * @param warningBytes the warning defined as the number of bytes, starting from zero and
+     *                     counting from now. A value of {@link #QUOTA_UNLIMITED} indicates
+     *                     there is no warning.
+     * @param limitBytes the limit defined as the number of bytes, starting from zero and counting
+     *                   from now. A value of {@link #QUOTA_UNLIMITED} indicates there is no limit.
+     */
+    public void onSetWarningAndLimit(@NonNull String iface, long warningBytes, long limitBytes) {
+        // Backward compatibility for those who didn't override this function.
+        onSetLimit(iface, limitBytes);
+    }
+
+    /**
+     * Called by {@code NetworkStatsService} when setting the alert bytes. Custom implementations
+     * MUST call {@link NetworkStatsProvider#notifyAlertReached()} when {@code quotaBytes} bytes
+     * have been reached. Unlike {@link #onSetLimit(String, long)}, the custom implementation should
+     * not block all egress packets.
+     *
+     * @param quotaBytes the quota defined as the number of bytes, starting from zero and counting
+     *                   from now. A value of {@link #QUOTA_UNLIMITED} indicates there is no alert.
+     */
+    public abstract void onSetAlert(long quotaBytes);
+}
diff --git a/framework-t/src/android/net/nsd/INsdManager.aidl b/framework-t/src/android/net/nsd/INsdManager.aidl
new file mode 100644
index 0000000..89e9cdb
--- /dev/null
+++ b/framework-t/src/android/net/nsd/INsdManager.aidl
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.net.nsd.INsdManagerCallback;
+import android.net.nsd.INsdServiceConnector;
+import android.os.Messenger;
+
+/**
+ * Interface that NsdService implements to connect NsdManager clients.
+ *
+ * {@hide}
+ */
+interface INsdManager {
+    INsdServiceConnector connect(INsdManagerCallback cb);
+}
diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
new file mode 100644
index 0000000..1a262ec
--- /dev/null
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.os.Messenger;
+import android.net.nsd.NsdServiceInfo;
+
+/**
+ * Callbacks from NsdService to NsdManager
+ * @hide
+ */
+oneway interface INsdManagerCallback {
+    void onDiscoverServicesStarted(int listenerKey, in NsdServiceInfo info);
+    void onDiscoverServicesFailed(int listenerKey, int error);
+    void onServiceFound(int listenerKey, in NsdServiceInfo info);
+    void onServiceLost(int listenerKey, in NsdServiceInfo info);
+    void onStopDiscoveryFailed(int listenerKey, int error);
+    void onStopDiscoverySucceeded(int listenerKey);
+    void onRegisterServiceFailed(int listenerKey, int error);
+    void onRegisterServiceSucceeded(int listenerKey, in NsdServiceInfo info);
+    void onUnregisterServiceFailed(int listenerKey, int error);
+    void onUnregisterServiceSucceeded(int listenerKey);
+    void onResolveServiceFailed(int listenerKey, int error);
+    void onResolveServiceSucceeded(int listenerKey, in NsdServiceInfo info);
+}
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
new file mode 100644
index 0000000..b06ae55
--- /dev/null
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.net.nsd.INsdManagerCallback;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Messenger;
+
+/**
+ * Interface that NsdService implements for each NsdManager client.
+ *
+ * {@hide}
+ */
+interface INsdServiceConnector {
+    void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+    void unregisterService(int listenerKey);
+    void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
+    void stopDiscovery(int listenerKey);
+    void resolveService(int listenerKey, in NsdServiceInfo serviceInfo);
+    void startDaemon();
+}
\ No newline at end of file
diff --git a/framework-t/src/android/net/nsd/MDnsManager.java b/framework-t/src/android/net/nsd/MDnsManager.java
new file mode 100644
index 0000000..c11e60c
--- /dev/null
+++ b/framework-t/src/android/net/nsd/MDnsManager.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.nsd;
+
+import android.annotation.NonNull;
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.IMDns;
+import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.RegistrationInfo;
+import android.net.mdns.aidl.ResolutionInfo;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+/**
+ * A manager class for mdns service.
+ *
+ * @hide
+ */
+public class MDnsManager {
+    private static final String TAG = MDnsManager.class.getSimpleName();
+    private final IMDns mMdns;
+
+    /** Service name for this. */
+    public static final String MDNS_SERVICE = "mdns";
+
+    private static final int NO_RESULT = -1;
+    private static final int NETID_UNSET = 0;
+
+    public MDnsManager(IMDns mdns) {
+        mMdns = mdns;
+    }
+
+    /**
+     * Start the MDNSResponder daemon.
+     */
+    public void startDaemon() {
+        try {
+            mMdns.startDaemon();
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Start mdns failed.", e);
+        }
+    }
+
+    /**
+     * Stop the MDNSResponder daemon.
+     */
+    public void stopDaemon() {
+        try {
+            mMdns.stopDaemon();
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Stop mdns failed.", e);
+        }
+    }
+
+    /**
+     * Start registering a service.
+     *
+     * @param id The operation ID.
+     * @param serviceName The service name to be registered.
+     * @param registrationType The service type to be registered.
+     * @param port The port on which the service accepts connections.
+     * @param txtRecord The txt record. Refer to {@code NsdServiceInfo#setTxtRecords} for details.
+     * @param interfaceIdx The interface index on which to register the service.
+     * @return {@code true} if registration is successful, else {@code false}.
+     */
+    public boolean registerService(int id, @NonNull String serviceName,
+            @NonNull String registrationType, int port, @NonNull byte[] txtRecord,
+            int interfaceIdx) {
+        final RegistrationInfo info = new RegistrationInfo(id, NO_RESULT, serviceName,
+                registrationType, port, txtRecord, interfaceIdx);
+        try {
+            mMdns.registerService(info);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Register service failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Start discovering services.
+     *
+     * @param id The operation ID.
+     * @param registrationType The service type to be discovered.
+     * @param interfaceIdx The interface index on which to discover for services.
+     * @return {@code true} if discovery is started successfully, else {@code false}.
+     */
+    public boolean discover(int id, @NonNull String registrationType, int interfaceIdx) {
+        final DiscoveryInfo info = new DiscoveryInfo(id, NO_RESULT, "" /* serviceName */,
+                registrationType, "" /* domainName */, interfaceIdx, NETID_UNSET);
+        try {
+            mMdns.discover(info);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Discover service failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Start resolving the target service.
+     *
+     * @param id The operation ID.
+     * @param serviceName The service name to be resolved.
+     * @param registrationType The service type to be resolved.
+     * @param domain The service domain to be resolved.
+     * @param interfaceIdx The interface index on which to resolve the service.
+     * @return {@code true} if resolution is started successfully, else {@code false}.
+     */
+    public boolean resolve(int id, @NonNull String serviceName, @NonNull String registrationType,
+            @NonNull String domain, int interfaceIdx) {
+        final ResolutionInfo info = new ResolutionInfo(id, NO_RESULT, serviceName,
+                registrationType, domain, "" /* serviceFullName */, "" /* hostname */, 0 /* port */,
+                new byte[0] /* txtRecord */, interfaceIdx);
+        try {
+            mMdns.resolve(info);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Resolve service failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Start getting the target service address.
+     *
+     * @param id The operation ID.
+     * @param hostname The fully qualified domain name of the host to be queried for.
+     * @param interfaceIdx The interface index on which to issue the query.
+     * @return {@code true} if getting address is started successful, else {@code false}.
+     */
+    public boolean getServiceAddress(int id, @NonNull String hostname, int interfaceIdx) {
+        final GetAddressInfo info = new GetAddressInfo(id, NO_RESULT, hostname,
+                "" /* address */, interfaceIdx, NETID_UNSET);
+        try {
+            mMdns.getServiceAddress(info);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Get service address failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Stop an operation which was requested before.
+     *
+     * @param id the operation id to be stopped.
+     * @return {@code true} if operation is stopped successfully, else {@code false}.
+     */
+    public boolean stopOperation(int id) {
+        try {
+            mMdns.stopOperation(id);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Stop operation failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Register an event listener.
+     *
+     * @param listener The listener to be registered.
+     */
+    public void registerEventListener(@NonNull IMDnsEventListener listener) {
+        try {
+            mMdns.registerEventListener(listener);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Register listener failed.", e);
+        }
+    }
+
+    /**
+     * Unregister an event listener.
+     *
+     * @param listener The listener to be unregistered.
+     */
+    public void unregisterEventListener(@NonNull IMDnsEventListener listener) {
+        try {
+            mMdns.unregisterEventListener(listener);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Unregister listener failed.", e);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
new file mode 100644
index 0000000..33b44c8
--- /dev/null
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -0,0 +1,1083 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemService;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * The Network Service Discovery Manager class provides the API to discover services
+ * on a network. As an example, if device A and device B are connected over a Wi-Fi
+ * network, a game registered on device A can be discovered by a game on device
+ * B. Another example use case is an application discovering printers on the network.
+ *
+ * <p> The API currently supports DNS based service discovery and discovery is currently
+ * limited to a local network over Multicast DNS. DNS service discovery is described at
+ * http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt
+ *
+ * <p> The API is asynchronous, and responses to requests from an application are on listener
+ * callbacks on a separate internal thread.
+ *
+ * <p> There are three main operations the API supports - registration, discovery and resolution.
+ * <pre>
+ *                          Application start
+ *                                 |
+ *                                 |
+ *                                 |                  onServiceRegistered()
+ *                     Register any local services  /
+ *                      to be advertised with       \
+ *                       registerService()            onRegistrationFailed()
+ *                                 |
+ *                                 |
+ *                          discoverServices()
+ *                                 |
+ *                      Maintain a list to track
+ *                        discovered services
+ *                                 |
+ *                                 |--------->
+ *                                 |          |
+ *                                 |      onServiceFound()
+ *                                 |          |
+ *                                 |     add service to list
+ *                                 |          |
+ *                                 |<----------
+ *                                 |
+ *                                 |--------->
+ *                                 |          |
+ *                                 |      onServiceLost()
+ *                                 |          |
+ *                                 |   remove service from list
+ *                                 |          |
+ *                                 |<----------
+ *                                 |
+ *                                 |
+ *                                 | Connect to a service
+ *                                 | from list ?
+ *                                 |
+ *                          resolveService()
+ *                                 |
+ *                         onServiceResolved()
+ *                                 |
+ *                     Establish connection to service
+ *                     with the host and port information
+ *
+ * </pre>
+ * An application that needs to advertise itself over a network for other applications to
+ * discover it can do so with a call to {@link #registerService}. If Example is a http based
+ * application that can provide HTML data to peer services, it can register a name "Example"
+ * with service type "_http._tcp". A successful registration is notified with a callback to
+ * {@link RegistrationListener#onServiceRegistered} and a failure to register is notified
+ * over {@link RegistrationListener#onRegistrationFailed}
+ *
+ * <p> A peer application looking for http services can initiate a discovery for "_http._tcp"
+ * with a call to {@link #discoverServices}. A service found is notified with a callback
+ * to {@link DiscoveryListener#onServiceFound} and a service lost is notified on
+ * {@link DiscoveryListener#onServiceLost}.
+ *
+ * <p> Once the peer application discovers the "Example" http service, and either needs to read the
+ * attributes of the service or wants to receive data from the "Example" application, it can
+ * initiate a resolve with {@link #resolveService} to resolve the attributes, host, and port
+ * details. A successful resolve is notified on {@link ResolveListener#onServiceResolved} and a
+ * failure is notified on {@link ResolveListener#onResolveFailed}.
+ *
+ * Applications can reserve for a service type at
+ * http://www.iana.org/form/ports-service. Existing services can be found at
+ * http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml
+ *
+ * {@see NsdServiceInfo}
+ */
+@SystemService(Context.NSD_SERVICE)
+public final class NsdManager {
+    private static final String TAG = NsdManager.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    /**
+     * When enabled, apps targeting < Android 12 are considered legacy for
+     * the NSD native daemon.
+     * The platform will only keep the daemon running as long as there are
+     * any legacy apps connected.
+     *
+     * After Android 12, directly communicate with native daemon might not
+     * work since the native damon won't always stay alive.
+     * Use the NSD APIs from NsdManager as the replacement is recommended.
+     * An another alternative could be bundling your own mdns solutions instead of
+     * depending on the system mdns native daemon.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.S)
+    public static final long RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS = 191844585L;
+
+    /**
+     * Broadcast intent action to indicate whether network service discovery is
+     * enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
+     * information as int.
+     *
+     * @see #EXTRA_NSD_STATE
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED";
+
+    /**
+     * The lookup key for an int that indicates whether network service discovery is enabled
+     * or disabled. Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}.
+     *
+     * @see #NSD_STATE_DISABLED
+     * @see #NSD_STATE_ENABLED
+     */
+    public static final String EXTRA_NSD_STATE = "nsd_state";
+
+    /**
+     * Network service discovery is disabled
+     *
+     * @see #ACTION_NSD_STATE_CHANGED
+     */
+    public static final int NSD_STATE_DISABLED = 1;
+
+    /**
+     * Network service discovery is enabled
+     *
+     * @see #ACTION_NSD_STATE_CHANGED
+     */
+    public static final int NSD_STATE_ENABLED = 2;
+
+    /** @hide */
+    public static final int DISCOVER_SERVICES                       = 1;
+    /** @hide */
+    public static final int DISCOVER_SERVICES_STARTED               = 2;
+    /** @hide */
+    public static final int DISCOVER_SERVICES_FAILED                = 3;
+    /** @hide */
+    public static final int SERVICE_FOUND                           = 4;
+    /** @hide */
+    public static final int SERVICE_LOST                            = 5;
+
+    /** @hide */
+    public static final int STOP_DISCOVERY                          = 6;
+    /** @hide */
+    public static final int STOP_DISCOVERY_FAILED                   = 7;
+    /** @hide */
+    public static final int STOP_DISCOVERY_SUCCEEDED                = 8;
+
+    /** @hide */
+    public static final int REGISTER_SERVICE                        = 9;
+    /** @hide */
+    public static final int REGISTER_SERVICE_FAILED                 = 10;
+    /** @hide */
+    public static final int REGISTER_SERVICE_SUCCEEDED              = 11;
+
+    /** @hide */
+    public static final int UNREGISTER_SERVICE                      = 12;
+    /** @hide */
+    public static final int UNREGISTER_SERVICE_FAILED               = 13;
+    /** @hide */
+    public static final int UNREGISTER_SERVICE_SUCCEEDED            = 14;
+
+    /** @hide */
+    public static final int RESOLVE_SERVICE                         = 15;
+    /** @hide */
+    public static final int RESOLVE_SERVICE_FAILED                  = 16;
+    /** @hide */
+    public static final int RESOLVE_SERVICE_SUCCEEDED               = 17;
+
+    /** @hide */
+    public static final int DAEMON_CLEANUP                          = 18;
+
+    /** @hide */
+    public static final int DAEMON_STARTUP                          = 19;
+
+    /** @hide */
+    public static final int ENABLE                                  = 20;
+    /** @hide */
+    public static final int DISABLE                                 = 21;
+
+    /** @hide */
+    public static final int MDNS_SERVICE_EVENT                      = 22;
+
+    /** @hide */
+    public static final int REGISTER_CLIENT                         = 23;
+    /** @hide */
+    public static final int UNREGISTER_CLIENT                       = 24;
+
+    /** Dns based service discovery protocol */
+    public static final int PROTOCOL_DNS_SD = 0x0001;
+
+    private static final SparseArray<String> EVENT_NAMES = new SparseArray<>();
+    static {
+        EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES");
+        EVENT_NAMES.put(DISCOVER_SERVICES_STARTED, "DISCOVER_SERVICES_STARTED");
+        EVENT_NAMES.put(DISCOVER_SERVICES_FAILED, "DISCOVER_SERVICES_FAILED");
+        EVENT_NAMES.put(SERVICE_FOUND, "SERVICE_FOUND");
+        EVENT_NAMES.put(SERVICE_LOST, "SERVICE_LOST");
+        EVENT_NAMES.put(STOP_DISCOVERY, "STOP_DISCOVERY");
+        EVENT_NAMES.put(STOP_DISCOVERY_FAILED, "STOP_DISCOVERY_FAILED");
+        EVENT_NAMES.put(STOP_DISCOVERY_SUCCEEDED, "STOP_DISCOVERY_SUCCEEDED");
+        EVENT_NAMES.put(REGISTER_SERVICE, "REGISTER_SERVICE");
+        EVENT_NAMES.put(REGISTER_SERVICE_FAILED, "REGISTER_SERVICE_FAILED");
+        EVENT_NAMES.put(REGISTER_SERVICE_SUCCEEDED, "REGISTER_SERVICE_SUCCEEDED");
+        EVENT_NAMES.put(UNREGISTER_SERVICE, "UNREGISTER_SERVICE");
+        EVENT_NAMES.put(UNREGISTER_SERVICE_FAILED, "UNREGISTER_SERVICE_FAILED");
+        EVENT_NAMES.put(UNREGISTER_SERVICE_SUCCEEDED, "UNREGISTER_SERVICE_SUCCEEDED");
+        EVENT_NAMES.put(RESOLVE_SERVICE, "RESOLVE_SERVICE");
+        EVENT_NAMES.put(RESOLVE_SERVICE_FAILED, "RESOLVE_SERVICE_FAILED");
+        EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED");
+        EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP");
+        EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
+        EVENT_NAMES.put(ENABLE, "ENABLE");
+        EVENT_NAMES.put(DISABLE, "DISABLE");
+        EVENT_NAMES.put(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT");
+    }
+
+    /** @hide */
+    public static String nameOf(int event) {
+        String name = EVENT_NAMES.get(event);
+        if (name == null) {
+            return Integer.toString(event);
+        }
+        return name;
+    }
+
+    private static final int FIRST_LISTENER_KEY = 1;
+
+    private final INsdServiceConnector mService;
+    private final Context mContext;
+
+    private int mListenerKey = FIRST_LISTENER_KEY;
+    @GuardedBy("mMapLock")
+    private final SparseArray mListenerMap = new SparseArray();
+    @GuardedBy("mMapLock")
+    private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>();
+    @GuardedBy("mMapLock")
+    private final SparseArray<Executor> mExecutorMap = new SparseArray<>();
+    private final Object mMapLock = new Object();
+    // Map of listener key sent by client -> per-network discovery tracker
+    @GuardedBy("mPerNetworkDiscoveryMap")
+    private final ArrayMap<Integer, PerNetworkDiscoveryTracker>
+            mPerNetworkDiscoveryMap = new ArrayMap<>();
+
+    private final ServiceHandler mHandler;
+
+    private class PerNetworkDiscoveryTracker {
+        final String mServiceType;
+        final int mProtocolType;
+        final DiscoveryListener mBaseListener;
+        final Executor mBaseExecutor;
+        final ArrayMap<Network, DelegatingDiscoveryListener> mPerNetworkListeners =
+                new ArrayMap<>();
+
+        final NetworkCallback mNetworkCb = new NetworkCallback() {
+            @Override
+            public void onAvailable(@NonNull Network network) {
+                final DelegatingDiscoveryListener wrappedListener = new DelegatingDiscoveryListener(
+                        network, mBaseListener);
+                mPerNetworkListeners.put(network, wrappedListener);
+                discoverServices(mServiceType, mProtocolType, network, mBaseExecutor,
+                        wrappedListener);
+            }
+
+            @Override
+            public void onLost(@NonNull Network network) {
+                final DelegatingDiscoveryListener listener = mPerNetworkListeners.get(network);
+                if (listener == null) return;
+                listener.notifyAllServicesLost();
+                // Listener will be removed from map in discovery stopped callback
+                stopServiceDiscovery(listener);
+            }
+        };
+
+        // Accessed from mHandler
+        private boolean mStopRequested;
+
+        public void start(@NonNull NetworkRequest request) {
+            final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+            cm.registerNetworkCallback(request, mNetworkCb, mHandler);
+            mHandler.post(() -> mBaseListener.onDiscoveryStarted(mServiceType));
+        }
+
+        /**
+         * Stop discovery on all networks tracked by this class.
+         *
+         * This will request all underlying listeners to stop, and the last one to stop will call
+         * onDiscoveryStopped or onStopDiscoveryFailed.
+         *
+         * Must be called on the handler thread.
+         */
+        public void requestStop() {
+            mHandler.post(() -> {
+                mStopRequested = true;
+                final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+                cm.unregisterNetworkCallback(mNetworkCb);
+                if (mPerNetworkListeners.size() == 0) {
+                    mBaseListener.onDiscoveryStopped(mServiceType);
+                    return;
+                }
+                for (int i = 0; i < mPerNetworkListeners.size(); i++) {
+                    final DelegatingDiscoveryListener listener = mPerNetworkListeners.valueAt(i);
+                    stopServiceDiscovery(listener);
+                }
+            });
+        }
+
+        private PerNetworkDiscoveryTracker(String serviceType, int protocolType,
+                Executor baseExecutor, DiscoveryListener baseListener) {
+            mServiceType = serviceType;
+            mProtocolType = protocolType;
+            mBaseExecutor = baseExecutor;
+            mBaseListener = baseListener;
+        }
+
+        /**
+         * Subset of NsdServiceInfo that is tracked to generate service lost notifications when a
+         * network is lost.
+         *
+         * Service lost notifications only contain service name, type and network, so only track
+         * that information (Network is known from the listener). This also implements
+         * equals/hashCode for usage in maps.
+         */
+        private class TrackedNsdInfo {
+            private final String mServiceName;
+            private final String mServiceType;
+            TrackedNsdInfo(NsdServiceInfo info) {
+                mServiceName = info.getServiceName();
+                mServiceType = info.getServiceType();
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mServiceName, mServiceType);
+            }
+
+            @Override
+            public boolean equals(Object obj) {
+                if (!(obj instanceof TrackedNsdInfo)) return false;
+                final TrackedNsdInfo other = (TrackedNsdInfo) obj;
+                return Objects.equals(mServiceName, other.mServiceName)
+                        && Objects.equals(mServiceType, other.mServiceType);
+            }
+        }
+
+        private class DelegatingDiscoveryListener implements DiscoveryListener {
+            private final Network mNetwork;
+            private final DiscoveryListener mWrapped;
+            private final ArraySet<TrackedNsdInfo> mFoundInfo = new ArraySet<>();
+
+            private DelegatingDiscoveryListener(Network network, DiscoveryListener listener) {
+                mNetwork = network;
+                mWrapped = listener;
+            }
+
+            void notifyAllServicesLost() {
+                for (int i = 0; i < mFoundInfo.size(); i++) {
+                    final TrackedNsdInfo trackedInfo = mFoundInfo.valueAt(i);
+                    final NsdServiceInfo serviceInfo = new NsdServiceInfo(
+                            trackedInfo.mServiceName, trackedInfo.mServiceType);
+                    serviceInfo.setNetwork(mNetwork);
+                    mWrapped.onServiceLost(serviceInfo);
+                }
+            }
+
+            @Override
+            public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+                // The delegated listener is used when NsdManager takes care of starting/stopping
+                // discovery on multiple networks. Failure to start on one network is not a global
+                // failure to be reported up, as other networks may succeed: just log.
+                Log.e(TAG, "Failed to start discovery for " + serviceType + " on " + mNetwork
+                        + " with code " + errorCode);
+                mPerNetworkListeners.remove(mNetwork);
+            }
+
+            @Override
+            public void onDiscoveryStarted(String serviceType) {
+                // Wrapped listener was called upon registration, it is not called for discovery
+                // on each network
+            }
+
+            @Override
+            public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+                Log.e(TAG, "Failed to stop discovery for " + serviceType + " on " + mNetwork
+                        + " with code " + errorCode);
+                mPerNetworkListeners.remove(mNetwork);
+                if (mStopRequested && mPerNetworkListeners.size() == 0) {
+                    // Do not report onStopDiscoveryFailed when some underlying listeners failed:
+                    // this does not mean that all listeners did, and onStopDiscoveryFailed is not
+                    // actionable anyway. Just report that discovery stopped.
+                    mWrapped.onDiscoveryStopped(serviceType);
+                }
+            }
+
+            @Override
+            public void onDiscoveryStopped(String serviceType) {
+                mPerNetworkListeners.remove(mNetwork);
+                if (mStopRequested && mPerNetworkListeners.size() == 0) {
+                    mWrapped.onDiscoveryStopped(serviceType);
+                }
+            }
+
+            @Override
+            public void onServiceFound(NsdServiceInfo serviceInfo) {
+                mFoundInfo.add(new TrackedNsdInfo(serviceInfo));
+                mWrapped.onServiceFound(serviceInfo);
+            }
+
+            @Override
+            public void onServiceLost(NsdServiceInfo serviceInfo) {
+                mFoundInfo.remove(new TrackedNsdInfo(serviceInfo));
+                mWrapped.onServiceLost(serviceInfo);
+            }
+        }
+    }
+
+    /**
+     * Create a new Nsd instance. Applications use
+     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+     * {@link android.content.Context#NSD_SERVICE Context.NSD_SERVICE}.
+     * @param service the Binder interface
+     * @hide - hide this because it takes in a parameter of type INsdManager, which
+     * is a system private class.
+     */
+    public NsdManager(Context context, INsdManager service) {
+        mContext = context;
+
+        HandlerThread t = new HandlerThread("NsdManager");
+        t.start();
+        mHandler = new ServiceHandler(t.getLooper());
+
+        try {
+            mService = service.connect(new NsdCallbackImpl(mHandler));
+        } catch (RemoteException e) {
+            throw new RuntimeException("Failed to connect to NsdService");
+        }
+
+        // Only proactively start the daemon if the target SDK < S, otherwise the internal service
+        // would automatically start/stop the native daemon as needed.
+        if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)) {
+            try {
+                mService.startDaemon();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to proactively start daemon");
+                // Continue: the daemon can still be started on-demand later
+            }
+        }
+    }
+
+    private static class NsdCallbackImpl extends INsdManagerCallback.Stub {
+        private final Handler mServHandler;
+
+        NsdCallbackImpl(Handler serviceHandler) {
+            mServHandler = serviceHandler;
+        }
+
+        private void sendInfo(int message, int listenerKey, NsdServiceInfo info) {
+            mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey, info));
+        }
+
+        private void sendError(int message, int listenerKey, int error) {
+            mServHandler.sendMessage(mServHandler.obtainMessage(message, error, listenerKey));
+        }
+
+        private void sendNoArg(int message, int listenerKey) {
+            mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey));
+        }
+
+        @Override
+        public void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
+            sendInfo(DISCOVER_SERVICES_STARTED, listenerKey, info);
+        }
+
+        @Override
+        public void onDiscoverServicesFailed(int listenerKey, int error) {
+            sendError(DISCOVER_SERVICES_FAILED, listenerKey, error);
+        }
+
+        @Override
+        public void onServiceFound(int listenerKey, NsdServiceInfo info) {
+            sendInfo(SERVICE_FOUND, listenerKey, info);
+        }
+
+        @Override
+        public void onServiceLost(int listenerKey, NsdServiceInfo info) {
+            sendInfo(SERVICE_LOST, listenerKey, info);
+        }
+
+        @Override
+        public void onStopDiscoveryFailed(int listenerKey, int error) {
+            sendError(STOP_DISCOVERY_FAILED, listenerKey, error);
+        }
+
+        @Override
+        public void onStopDiscoverySucceeded(int listenerKey) {
+            sendNoArg(STOP_DISCOVERY_SUCCEEDED, listenerKey);
+        }
+
+        @Override
+        public void onRegisterServiceFailed(int listenerKey, int error) {
+            sendError(REGISTER_SERVICE_FAILED, listenerKey, error);
+        }
+
+        @Override
+        public void onRegisterServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+            sendInfo(REGISTER_SERVICE_SUCCEEDED, listenerKey, info);
+        }
+
+        @Override
+        public void onUnregisterServiceFailed(int listenerKey, int error) {
+            sendError(UNREGISTER_SERVICE_FAILED, listenerKey, error);
+        }
+
+        @Override
+        public void onUnregisterServiceSucceeded(int listenerKey) {
+            sendNoArg(UNREGISTER_SERVICE_SUCCEEDED, listenerKey);
+        }
+
+        @Override
+        public void onResolveServiceFailed(int listenerKey, int error) {
+            sendError(RESOLVE_SERVICE_FAILED, listenerKey, error);
+        }
+
+        @Override
+        public void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+            sendInfo(RESOLVE_SERVICE_SUCCEEDED, listenerKey, info);
+        }
+    }
+
+    /**
+     * Failures are passed with {@link RegistrationListener#onRegistrationFailed},
+     * {@link RegistrationListener#onUnregistrationFailed},
+     * {@link DiscoveryListener#onStartDiscoveryFailed},
+     * {@link DiscoveryListener#onStopDiscoveryFailed} or {@link ResolveListener#onResolveFailed}.
+     *
+     * Indicates that the operation failed due to an internal error.
+     */
+    public static final int FAILURE_INTERNAL_ERROR               = 0;
+
+    /**
+     * Indicates that the operation failed because it is already active.
+     */
+    public static final int FAILURE_ALREADY_ACTIVE              = 3;
+
+    /**
+     * Indicates that the operation failed because the maximum outstanding
+     * requests from the applications have reached.
+     */
+    public static final int FAILURE_MAX_LIMIT                   = 4;
+
+    /** Interface for callback invocation for service discovery */
+    public interface DiscoveryListener {
+
+        public void onStartDiscoveryFailed(String serviceType, int errorCode);
+
+        public void onStopDiscoveryFailed(String serviceType, int errorCode);
+
+        public void onDiscoveryStarted(String serviceType);
+
+        public void onDiscoveryStopped(String serviceType);
+
+        public void onServiceFound(NsdServiceInfo serviceInfo);
+
+        public void onServiceLost(NsdServiceInfo serviceInfo);
+    }
+
+    /** Interface for callback invocation for service registration */
+    public interface RegistrationListener {
+
+        public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+        public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+        public void onServiceRegistered(NsdServiceInfo serviceInfo);
+
+        public void onServiceUnregistered(NsdServiceInfo serviceInfo);
+    }
+
+    /** Interface for callback invocation for service resolution */
+    public interface ResolveListener {
+
+        public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+        public void onServiceResolved(NsdServiceInfo serviceInfo);
+    }
+
+    @VisibleForTesting
+    class ServiceHandler extends Handler {
+        ServiceHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            final int what = message.what;
+            final int key = message.arg2;
+            final Object listener;
+            final NsdServiceInfo ns;
+            final Executor executor;
+            synchronized (mMapLock) {
+                listener = mListenerMap.get(key);
+                ns = mServiceMap.get(key);
+                executor = mExecutorMap.get(key);
+            }
+            if (listener == null) {
+                Log.d(TAG, "Stale key " + message.arg2);
+                return;
+            }
+            if (DBG) {
+                Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+            }
+            switch (what) {
+                case DISCOVER_SERVICES_STARTED:
+                    final String s = getNsdServiceInfoType((NsdServiceInfo) message.obj);
+                    executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s));
+                    break;
+                case DISCOVER_SERVICES_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed(
+                            getNsdServiceInfoType(ns), message.arg1));
+                    break;
+                case SERVICE_FOUND:
+                    executor.execute(() -> ((DiscoveryListener) listener).onServiceFound(
+                            (NsdServiceInfo) message.obj));
+                    break;
+                case SERVICE_LOST:
+                    executor.execute(() -> ((DiscoveryListener) listener).onServiceLost(
+                            (NsdServiceInfo) message.obj));
+                    break;
+                case STOP_DISCOVERY_FAILED:
+                    // TODO: failure to stop discovery should be internal and retried internally, as
+                    // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
+                    removeListener(key);
+                    executor.execute(() -> ((DiscoveryListener) listener).onStopDiscoveryFailed(
+                            getNsdServiceInfoType(ns), message.arg1));
+                    break;
+                case STOP_DISCOVERY_SUCCEEDED:
+                    removeListener(key);
+                    executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStopped(
+                            getNsdServiceInfoType(ns)));
+                    break;
+                case REGISTER_SERVICE_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((RegistrationListener) listener).onRegistrationFailed(
+                            ns, message.arg1));
+                    break;
+                case REGISTER_SERVICE_SUCCEEDED:
+                    executor.execute(() -> ((RegistrationListener) listener).onServiceRegistered(
+                            (NsdServiceInfo) message.obj));
+                    break;
+                case UNREGISTER_SERVICE_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((RegistrationListener) listener).onUnregistrationFailed(
+                            ns, message.arg1));
+                    break;
+                case UNREGISTER_SERVICE_SUCCEEDED:
+                    // TODO: do not unregister listener until service is unregistered, or provide
+                    // alternative way for unregistering ?
+                    removeListener(message.arg2);
+                    executor.execute(() -> ((RegistrationListener) listener).onServiceUnregistered(
+                            ns));
+                    break;
+                case RESOLVE_SERVICE_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((ResolveListener) listener).onResolveFailed(
+                            ns, message.arg1));
+                    break;
+                case RESOLVE_SERVICE_SUCCEEDED:
+                    removeListener(key);
+                    executor.execute(() -> ((ResolveListener) listener).onServiceResolved(
+                            (NsdServiceInfo) message.obj));
+                    break;
+                default:
+                    Log.d(TAG, "Ignored " + message);
+                    break;
+            }
+        }
+    }
+
+    private int nextListenerKey() {
+        // Ensure mListenerKey >= FIRST_LISTENER_KEY;
+        mListenerKey = Math.max(FIRST_LISTENER_KEY, mListenerKey + 1);
+        return mListenerKey;
+    }
+
+    // Assert that the listener is not in the map, then add it and returns its key
+    private int putListener(Object listener, Executor e, NsdServiceInfo s) {
+        checkListener(listener);
+        final int key;
+        synchronized (mMapLock) {
+            int valueIndex = mListenerMap.indexOfValue(listener);
+            if (valueIndex != -1) {
+                throw new IllegalArgumentException("listener already in use");
+            }
+            key = nextListenerKey();
+            mListenerMap.put(key, listener);
+            mServiceMap.put(key, s);
+            mExecutorMap.put(key, e);
+        }
+        return key;
+    }
+
+    private void removeListener(int key) {
+        synchronized (mMapLock) {
+            mListenerMap.remove(key);
+            mServiceMap.remove(key);
+            mExecutorMap.remove(key);
+        }
+    }
+
+    private int getListenerKey(Object listener) {
+        checkListener(listener);
+        synchronized (mMapLock) {
+            int valueIndex = mListenerMap.indexOfValue(listener);
+            if (valueIndex == -1) {
+                throw new IllegalArgumentException("listener not registered");
+            }
+            return mListenerMap.keyAt(valueIndex);
+        }
+    }
+
+    private static String getNsdServiceInfoType(NsdServiceInfo s) {
+        if (s == null) return "?";
+        return s.getServiceType();
+    }
+
+    /**
+     * Register a service to be discovered by other services.
+     *
+     * <p> The function call immediately returns after sending a request to register service
+     * to the framework. The application is notified of a successful registration
+     * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+     * through {@link RegistrationListener#onRegistrationFailed}.
+     *
+     * <p> The application should call {@link #unregisterService} when the service
+     * registration is no longer required, and/or whenever the application is stopped.
+     *
+     * @param serviceInfo The service being registered
+     * @param protocolType The service discovery protocol
+     * @param listener The listener notifies of a successful registration and is used to
+     * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+     * Cannot be in use for an active service registration.
+     */
+    public void registerService(NsdServiceInfo serviceInfo, int protocolType,
+            RegistrationListener listener) {
+        registerService(serviceInfo, protocolType, Runnable::run, listener);
+    }
+
+    /**
+     * Register a service to be discovered by other services.
+     *
+     * <p> The function call immediately returns after sending a request to register service
+     * to the framework. The application is notified of a successful registration
+     * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+     * through {@link RegistrationListener#onRegistrationFailed}.
+     *
+     * <p> The application should call {@link #unregisterService} when the service
+     * registration is no longer required, and/or whenever the application is stopped.
+     * @param serviceInfo The service being registered
+     * @param protocolType The service discovery protocol
+     * @param executor Executor to run listener callbacks with
+     * @param listener The listener notifies of a successful registration and is used to
+     * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+     */
+    public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+            @NonNull Executor executor, @NonNull RegistrationListener listener) {
+        if (serviceInfo.getPort() <= 0) {
+            throw new IllegalArgumentException("Invalid port number");
+        }
+        checkServiceInfo(serviceInfo);
+        checkProtocol(protocolType);
+        int key = putListener(listener, executor, serviceInfo);
+        try {
+            mService.registerService(key, serviceInfo);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregister a service registered through {@link #registerService}. A successful
+     * unregister is notified to the application with a call to
+     * {@link RegistrationListener#onServiceUnregistered}.
+     *
+     * @param listener This should be the listener object that was passed to
+     * {@link #registerService}. It identifies the service that should be unregistered
+     * and notifies of a successful or unsuccessful unregistration via the listener
+     * callbacks.  In API versions 20 and above, the listener object may be used for
+     * another service registration once the callback has been called.  In API versions <= 19,
+     * there is no entirely reliable way to know when a listener may be re-used, and a new
+     * listener should be created for each service registration request.
+     */
+    public void unregisterService(RegistrationListener listener) {
+        int id = getListenerKey(listener);
+        try {
+            mService.unregisterService(id);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Initiate service discovery to browse for instances of a service type. Service discovery
+     * consumes network bandwidth and will continue until the application calls
+     * {@link #stopServiceDiscovery}.
+     *
+     * <p> The function call immediately returns after sending a request to start service
+     * discovery to the framework. The application is notified of a success to initiate
+     * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
+     * through {@link DiscoveryListener#onStartDiscoveryFailed}.
+     *
+     * <p> Upon successful start, application is notified when a service is found with
+     * {@link DiscoveryListener#onServiceFound} or when a service is lost with
+     * {@link DiscoveryListener#onServiceLost}.
+     *
+     * <p> Upon failure to start, service discovery is not active and application does
+     * not need to invoke {@link #stopServiceDiscovery}
+     *
+     * <p> The application should call {@link #stopServiceDiscovery} when discovery of this
+     * service type is no longer required, and/or whenever the application is paused or
+     * stopped.
+     *
+     * @param serviceType The service type being discovered. Examples include "_http._tcp" for
+     * http services or "_ipp._tcp" for printers
+     * @param protocolType The service discovery protocol
+     * @param listener  The listener notifies of a successful discovery and is used
+     * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
+     * Cannot be null. Cannot be in use for an active service discovery.
+     */
+    public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) {
+        discoverServices(serviceType, protocolType, (Network) null, Runnable::run, listener);
+    }
+
+    /**
+     * Initiate service discovery to browse for instances of a service type. Service discovery
+     * consumes network bandwidth and will continue until the application calls
+     * {@link #stopServiceDiscovery}.
+     *
+     * <p> The function call immediately returns after sending a request to start service
+     * discovery to the framework. The application is notified of a success to initiate
+     * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
+     * through {@link DiscoveryListener#onStartDiscoveryFailed}.
+     *
+     * <p> Upon successful start, application is notified when a service is found with
+     * {@link DiscoveryListener#onServiceFound} or when a service is lost with
+     * {@link DiscoveryListener#onServiceLost}.
+     *
+     * <p> Upon failure to start, service discovery is not active and application does
+     * not need to invoke {@link #stopServiceDiscovery}
+     *
+     * <p> The application should call {@link #stopServiceDiscovery} when discovery of this
+     * service type is no longer required, and/or whenever the application is paused or
+     * stopped.
+     * @param serviceType The service type being discovered. Examples include "_http._tcp" for
+     * http services or "_ipp._tcp" for printers
+     * @param protocolType The service discovery protocol
+     * @param network Network to discover services on, or null to discover on all available networks
+     * @param executor Executor to run listener callbacks with
+     * @param listener  The listener notifies of a successful discovery and is used
+     * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
+     */
+    public void discoverServices(@NonNull String serviceType, int protocolType,
+            @Nullable Network network, @NonNull Executor executor,
+            @NonNull DiscoveryListener listener) {
+        if (TextUtils.isEmpty(serviceType)) {
+            throw new IllegalArgumentException("Service type cannot be empty");
+        }
+        checkProtocol(protocolType);
+
+        NsdServiceInfo s = new NsdServiceInfo();
+        s.setServiceType(serviceType);
+        s.setNetwork(network);
+
+        int key = putListener(listener, executor, s);
+        try {
+            mService.discoverServices(key, s);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Initiate service discovery to browse for instances of a service type. Service discovery
+     * consumes network bandwidth and will continue until the application calls
+     * {@link #stopServiceDiscovery}.
+     *
+     * <p> The function call immediately returns after sending a request to start service
+     * discovery to the framework. The application is notified of a success to initiate
+     * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
+     * through {@link DiscoveryListener#onStartDiscoveryFailed}.
+     *
+     * <p> Upon successful start, application is notified when a service is found with
+     * {@link DiscoveryListener#onServiceFound} or when a service is lost with
+     * {@link DiscoveryListener#onServiceLost}.
+     *
+     * <p> Upon failure to start, service discovery is not active and application does
+     * not need to invoke {@link #stopServiceDiscovery}
+     *
+     * <p> The application should call {@link #stopServiceDiscovery} when discovery of this
+     * service type is no longer required, and/or whenever the application is paused or
+     * stopped.
+     *
+     * <p> During discovery, new networks may connect or existing networks may disconnect - for
+     * example if wifi is reconnected. When a service was found on a network that disconnects,
+     * {@link DiscoveryListener#onServiceLost} will be called. If a new network connects that
+     * matches the {@link NetworkRequest}, {@link DiscoveryListener#onServiceFound} will be called
+     * for services found on that network. Applications that do not want to track networks
+     * themselves are encouraged to use this method instead of other overloads of
+     * {@code discoverServices}, as they will receive proper notifications when a service becomes
+     * available or unavailable due to network changes.
+     * @param serviceType The service type being discovered. Examples include "_http._tcp" for
+     * http services or "_ipp._tcp" for printers
+     * @param protocolType The service discovery protocol
+     * @param networkRequest Request specifying networks that should be considered when discovering
+     * @param executor Executor to run listener callbacks with
+     * @param listener  The listener notifies of a successful discovery and is used
+     * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void discoverServices(@NonNull String serviceType, int protocolType,
+            @NonNull NetworkRequest networkRequest, @NonNull Executor executor,
+            @NonNull DiscoveryListener listener) {
+        if (TextUtils.isEmpty(serviceType)) {
+            throw new IllegalArgumentException("Service type cannot be empty");
+        }
+        Objects.requireNonNull(networkRequest, "NetworkRequest cannot be null");
+        checkProtocol(protocolType);
+
+        NsdServiceInfo s = new NsdServiceInfo();
+        s.setServiceType(serviceType);
+
+        final int baseListenerKey = putListener(listener, executor, s);
+
+        final PerNetworkDiscoveryTracker discoveryInfo = new PerNetworkDiscoveryTracker(
+                serviceType, protocolType, executor, listener);
+
+        synchronized (mPerNetworkDiscoveryMap) {
+            mPerNetworkDiscoveryMap.put(baseListenerKey, discoveryInfo);
+            discoveryInfo.start(networkRequest);
+        }
+    }
+
+    /**
+     * Stop service discovery initiated with {@link #discoverServices}.  An active service
+     * discovery is notified to the application with {@link DiscoveryListener#onDiscoveryStarted}
+     * and it stays active until the application invokes a stop service discovery. A successful
+     * stop is notified to with a call to {@link DiscoveryListener#onDiscoveryStopped}.
+     *
+     * <p> Upon failure to stop service discovery, application is notified through
+     * {@link DiscoveryListener#onStopDiscoveryFailed}.
+     *
+     * @param listener This should be the listener object that was passed to {@link #discoverServices}.
+     * It identifies the discovery that should be stopped and notifies of a successful or
+     * unsuccessful stop.  In API versions 20 and above, the listener object may be used for
+     * another service discovery once the callback has been called.  In API versions <= 19,
+     * there is no entirely reliable way to know when a listener may be re-used, and a new
+     * listener should be created for each service discovery request.
+     */
+    public void stopServiceDiscovery(DiscoveryListener listener) {
+        int id = getListenerKey(listener);
+        // If this is a PerNetworkDiscovery request, handle it as such
+        synchronized (mPerNetworkDiscoveryMap) {
+            final PerNetworkDiscoveryTracker info = mPerNetworkDiscoveryMap.get(id);
+            if (info != null) {
+                info.requestStop();
+                return;
+            }
+        }
+        try {
+            mService.stopDiscovery(id);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Resolve a discovered service. An application can resolve a service right before
+     * establishing a connection to fetch the IP and port details on which to setup
+     * the connection.
+     *
+     * @param serviceInfo service to be resolved
+     * @param listener to receive callback upon success or failure. Cannot be null.
+     * Cannot be in use for an active service resolution.
+     */
+    public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
+        resolveService(serviceInfo, Runnable::run, listener);
+    }
+
+    /**
+     * Resolve a discovered service. An application can resolve a service right before
+     * establishing a connection to fetch the IP and port details on which to setup
+     * the connection.
+     * @param serviceInfo service to be resolved
+     * @param executor Executor to run listener callbacks with
+     * @param listener to receive callback upon success or failure.
+     */
+    public void resolveService(@NonNull NsdServiceInfo serviceInfo,
+            @NonNull Executor executor, @NonNull ResolveListener listener) {
+        checkServiceInfo(serviceInfo);
+        int key = putListener(listener, executor, serviceInfo);
+        try {
+            mService.resolveService(key, serviceInfo);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    private static void checkListener(Object listener) {
+        Objects.requireNonNull(listener, "listener cannot be null");
+    }
+
+    private static void checkProtocol(int protocolType) {
+        if (protocolType != PROTOCOL_DNS_SD) {
+            throw new IllegalArgumentException("Unsupported protocol");
+        }
+    }
+
+    private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
+        Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
+        if (TextUtils.isEmpty(serviceInfo.getServiceName())) {
+            throw new IllegalArgumentException("Service name cannot be empty");
+        }
+        if (TextUtils.isEmpty(serviceInfo.getServiceType())) {
+            throw new IllegalArgumentException("Service type cannot be empty");
+        }
+    }
+}
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
new file mode 100644
index 0000000..2621594
--- /dev/null
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.net.Network;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A class representing service information for network service discovery
+ * {@see NsdManager}
+ */
+public final class NsdServiceInfo implements Parcelable {
+
+    private static final String TAG = "NsdServiceInfo";
+
+    private String mServiceName;
+
+    private String mServiceType;
+
+    private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
+
+    private InetAddress mHost;
+
+    private int mPort;
+
+    @Nullable
+    private Network mNetwork;
+
+    public NsdServiceInfo() {
+    }
+
+    /** @hide */
+    public NsdServiceInfo(String sn, String rt) {
+        mServiceName = sn;
+        mServiceType = rt;
+    }
+
+    /** Get the service name */
+    public String getServiceName() {
+        return mServiceName;
+    }
+
+    /** Set the service name */
+    public void setServiceName(String s) {
+        mServiceName = s;
+    }
+
+    /** Get the service type */
+    public String getServiceType() {
+        return mServiceType;
+    }
+
+    /** Set the service type */
+    public void setServiceType(String s) {
+        mServiceType = s;
+    }
+
+    /** Get the host address. The host address is valid for a resolved service. */
+    public InetAddress getHost() {
+        return mHost;
+    }
+
+    /** Set the host address */
+    public void setHost(InetAddress s) {
+        mHost = s;
+    }
+
+    /** Get port number. The port number is valid for a resolved service. */
+    public int getPort() {
+        return mPort;
+    }
+
+    /** Set port number */
+    public void setPort(int p) {
+        mPort = p;
+    }
+
+    /**
+     * Unpack txt information from a base-64 encoded byte array.
+     *
+     * @param txtRecordsRawBytes The raw base64 encoded byte array.
+     *
+     * @hide
+     */
+    public void setTxtRecords(@NonNull byte[] txtRecordsRawBytes) {
+        // There can be multiple TXT records after each other. Each record has to following format:
+        //
+        // byte                  type                  required   meaning
+        // -------------------   -------------------   --------   ----------------------------------
+        // 0                     unsigned 8 bit        yes        size of record excluding this byte
+        // 1 - n                 ASCII but not '='     yes        key
+        // n + 1                 '='                   optional   separator of key and value
+        // n + 2 - record size   uninterpreted bytes   optional   value
+        //
+        // Example legal records:
+        // [11, 'm', 'y', 'k', 'e', 'y', '=', 0x0, 0x4, 0x65, 0x7, 0xff]
+        // [17, 'm', 'y', 'K', 'e', 'y', 'W', 'i', 't', 'h', 'N', 'o', 'V', 'a', 'l', 'u', 'e', '=']
+        // [12, 'm', 'y', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'K', 'e', 'y']
+        //
+        // Example corrupted records
+        // [3, =, 1, 2]    <- key is empty
+        // [3, 0, =, 2]    <- key contains non-ASCII character. We handle this by replacing the
+        //                    invalid characters instead of skipping the record.
+        // [30, 'a', =, 2] <- length exceeds total left over bytes in the TXT records array, we
+        //                    handle this by reducing the length of the record as needed.
+        int pos = 0;
+        while (pos < txtRecordsRawBytes.length) {
+            // recordLen is an unsigned 8 bit value
+            int recordLen = txtRecordsRawBytes[pos] & 0xff;
+            pos += 1;
+
+            try {
+                if (recordLen == 0) {
+                    throw new IllegalArgumentException("Zero sized txt record");
+                } else if (pos + recordLen > txtRecordsRawBytes.length) {
+                    Log.w(TAG, "Corrupt record length (pos = " + pos + "): " + recordLen);
+                    recordLen = txtRecordsRawBytes.length - pos;
+                }
+
+                // Decode key-value records
+                String key = null;
+                byte[] value = null;
+                int valueLen = 0;
+                for (int i = pos; i < pos + recordLen; i++) {
+                    if (key == null) {
+                        if (txtRecordsRawBytes[i] == '=') {
+                            key = new String(txtRecordsRawBytes, pos, i - pos,
+                                    StandardCharsets.US_ASCII);
+                        }
+                    } else {
+                        if (value == null) {
+                            value = new byte[recordLen - key.length() - 1];
+                        }
+                        value[valueLen] = txtRecordsRawBytes[i];
+                        valueLen++;
+                    }
+                }
+
+                // If '=' was not found we have a boolean record
+                if (key == null) {
+                    key = new String(txtRecordsRawBytes, pos, recordLen, StandardCharsets.US_ASCII);
+                }
+
+                if (TextUtils.isEmpty(key)) {
+                    // Empty keys are not allowed (RFC6763 6.4)
+                    throw new IllegalArgumentException("Invalid txt record (key is empty)");
+                }
+
+                if (getAttributes().containsKey(key)) {
+                    // When we have a duplicate record, the later ones are ignored (RFC6763 6.4)
+                    throw new IllegalArgumentException("Invalid txt record (duplicate key \"" + key + "\")");
+                }
+
+                setAttribute(key, value);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "While parsing txt records (pos = " + pos + "): " + e.getMessage());
+            }
+
+            pos += recordLen;
+        }
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setAttribute(String key, byte[] value) {
+        if (TextUtils.isEmpty(key)) {
+            throw new IllegalArgumentException("Key cannot be empty");
+        }
+
+        // Key must be printable US-ASCII, excluding =.
+        for (int i = 0; i < key.length(); ++i) {
+            char character = key.charAt(i);
+            if (character < 0x20 || character > 0x7E) {
+                throw new IllegalArgumentException("Key strings must be printable US-ASCII");
+            } else if (character == 0x3D) {
+                throw new IllegalArgumentException("Key strings must not include '='");
+            }
+        }
+
+        // Key length + value length must be < 255.
+        if (key.length() + (value == null ? 0 : value.length) >= 255) {
+            throw new IllegalArgumentException("Key length + value length must be < 255 bytes");
+        }
+
+        // Warn if key is > 9 characters, as recommended by RFC 6763 section 6.4.
+        if (key.length() > 9) {
+            Log.w(TAG, "Key lengths > 9 are discouraged: " + key);
+        }
+
+        // Check against total TXT record size limits.
+        // Arbitrary 400 / 1300 byte limits taken from RFC 6763 section 6.2.
+        int txtRecordSize = getTxtRecordSize();
+        int futureSize = txtRecordSize + key.length() + (value == null ? 0 : value.length) + 2;
+        if (futureSize > 1300) {
+            throw new IllegalArgumentException("Total length of attributes must be < 1300 bytes");
+        } else if (futureSize > 400) {
+            Log.w(TAG, "Total length of all attributes exceeds 400 bytes; truncation may occur");
+        }
+
+        mTxtRecord.put(key, value);
+    }
+
+    /**
+     * Add a service attribute as a key/value pair.
+     *
+     * <p> Service attributes are included as DNS-SD TXT record pairs.
+     *
+     * <p> The key must be US-ASCII printable characters, excluding the '=' character.  Values may
+     * be UTF-8 strings or null.  The total length of key + value must be less than 255 bytes.
+     *
+     * <p> Keys should be short, ideally no more than 9 characters, and unique per instance of
+     * {@link NsdServiceInfo}.  Calling {@link #setAttribute} twice with the same key will overwrite
+     * first value.
+     */
+    public void setAttribute(String key, String value) {
+        try {
+            setAttribute(key, value == null ? (byte []) null : value.getBytes("UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new IllegalArgumentException("Value must be UTF-8");
+        }
+    }
+
+    /** Remove an attribute by key */
+    public void removeAttribute(String key) {
+        mTxtRecord.remove(key);
+    }
+
+    /**
+     * Retrieve attributes as a map of String keys to byte[] values. The attributes map is only
+     * valid for a resolved service.
+     *
+     * <p> The returned map is unmodifiable; changes must be made through {@link #setAttribute} and
+     * {@link #removeAttribute}.
+     */
+    public Map<String, byte[]> getAttributes() {
+        return Collections.unmodifiableMap(mTxtRecord);
+    }
+
+    private int getTxtRecordSize() {
+        int txtRecordSize = 0;
+        for (Map.Entry<String, byte[]> entry : mTxtRecord.entrySet()) {
+            txtRecordSize += 2;  // One for the length byte, one for the = between key and value.
+            txtRecordSize += entry.getKey().length();
+            byte[] value = entry.getValue();
+            txtRecordSize += value == null ? 0 : value.length;
+        }
+        return txtRecordSize;
+    }
+
+    /** @hide */
+    public @NonNull byte[] getTxtRecord() {
+        int txtRecordSize = getTxtRecordSize();
+        if (txtRecordSize == 0) {
+            return new byte[]{};
+        }
+
+        byte[] txtRecord = new byte[txtRecordSize];
+        int ptr = 0;
+        for (Map.Entry<String, byte[]> entry : mTxtRecord.entrySet()) {
+            String key = entry.getKey();
+            byte[] value = entry.getValue();
+
+            // One byte to record the length of this key/value pair.
+            txtRecord[ptr++] = (byte) (key.length() + (value == null ? 0 : value.length) + 1);
+
+            // The key, in US-ASCII.
+            // Note: use the StandardCharsets const here because it doesn't raise exceptions and we
+            // already know the key is ASCII at this point.
+            System.arraycopy(key.getBytes(StandardCharsets.US_ASCII), 0, txtRecord, ptr,
+                    key.length());
+            ptr += key.length();
+
+            // US-ASCII '=' character.
+            txtRecord[ptr++] = (byte)'=';
+
+            // The value, as any raw bytes.
+            if (value != null) {
+                System.arraycopy(value, 0, txtRecord, ptr, value.length);
+                ptr += value.length;
+            }
+        }
+        return txtRecord;
+    }
+
+    /**
+     * Get the network where the service can be found.
+     *
+     * This is never null if this {@link NsdServiceInfo} was obtained from
+     * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}.
+     */
+    @Nullable
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    /**
+     * Set the network where the service can be found.
+     * @param network The network, or null to search for, or to announce, the service on all
+     *                connected networks.
+     */
+    public void setNetwork(@Nullable Network network) {
+        mNetwork = network;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("name: ").append(mServiceName)
+                .append(", type: ").append(mServiceType)
+                .append(", host: ").append(mHost)
+                .append(", port: ").append(mPort)
+                .append(", network: ").append(mNetwork);
+
+        byte[] txtRecord = getTxtRecord();
+        sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
+        return sb.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mServiceName);
+        dest.writeString(mServiceType);
+        if (mHost != null) {
+            dest.writeInt(1);
+            dest.writeByteArray(mHost.getAddress());
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeInt(mPort);
+
+        // TXT record key/value pairs.
+        dest.writeInt(mTxtRecord.size());
+        for (String key : mTxtRecord.keySet()) {
+            byte[] value = mTxtRecord.get(key);
+            if (value != null) {
+                dest.writeInt(1);
+                dest.writeInt(value.length);
+                dest.writeByteArray(value);
+            } else {
+                dest.writeInt(0);
+            }
+            dest.writeString(key);
+        }
+
+        dest.writeParcelable(mNetwork, 0);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<NsdServiceInfo> CREATOR =
+        new Creator<NsdServiceInfo>() {
+            public NsdServiceInfo createFromParcel(Parcel in) {
+                NsdServiceInfo info = new NsdServiceInfo();
+                info.mServiceName = in.readString();
+                info.mServiceType = in.readString();
+
+                if (in.readInt() == 1) {
+                    try {
+                        info.mHost = InetAddress.getByAddress(in.createByteArray());
+                    } catch (java.net.UnknownHostException e) {}
+                }
+
+                info.mPort = in.readInt();
+
+                // TXT record key/value pairs.
+                int recordCount = in.readInt();
+                for (int i = 0; i < recordCount; ++i) {
+                    byte[] valueArray = null;
+                    if (in.readInt() == 1) {
+                        int valueLength = in.readInt();
+                        valueArray = new byte[valueLength];
+                        in.readByteArray(valueArray);
+                    }
+                    info.mTxtRecord.put(in.readString(), valueArray);
+                }
+                info.mNetwork = in.readParcelable(null, Network.class);
+                return info;
+            }
+
+            public NsdServiceInfo[] newArray(int size) {
+                return new NsdServiceInfo[size];
+            }
+        };
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index da16a8d..d7de439 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -63,6 +63,8 @@
     srcs: [
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
+        ":framework-connectivity-api-shared-srcs",
+        ":framework-connectivity-javastream-protos",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -76,7 +78,7 @@
         ],
     },
     stub_only_libs: [
-        "framework-connectivity-tiramisu.stubs.module_lib",
+        "framework-connectivity-t.stubs.module_lib",
     ],
     impl_only_libs: [
         "framework-tethering.stubs.module_lib",
@@ -84,11 +86,14 @@
         "net-utils-device-common",
     ],
     static_libs: [
+        "mdns_aidl_interface-lateststable-java",
+        "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
     ],
     libs: [
-        "framework-connectivity-tiramisu.stubs.module_lib",
+        "app-compat-annotations",
+        "framework-connectivity-t.stubs.module_lib",
         "unsupportedappusage",
     ],
     apex_available: [
@@ -137,6 +142,7 @@
         "//frameworks/base/tests/vcn",
         "//frameworks/libs/net/common/testutils",
         "//frameworks/libs/net/common/tests:__subpackages__",
+        "//frameworks/opt/net/ethernet/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
         "//packages/modules/CaptivePortalLogin/tests",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
@@ -147,6 +153,11 @@
     ],
 }
 
+platform_compat_config {
+    name: "connectivity-platform-compat-config",
+    src: ":framework-connectivity",
+}
+
 cc_library_shared {
     name: "libframework-connectivity-jni",
     min_sdk_version: "30",
@@ -177,3 +188,37 @@
         "com.android.tethering",
     ],
 }
+
+filegroup {
+    name: "framework-connectivity-protos",
+    srcs: [
+        "proto/**/*.proto",
+    ],
+    visibility: ["//frameworks/base"],
+}
+
+gensrcs {
+    name: "framework-connectivity-javastream-protos",
+    depfile: true,
+
+    tools: [
+        "aprotoc",
+        "protoc-gen-javastream",
+        "soong_zip",
+    ],
+
+    cmd: "mkdir -p $(genDir)/$(in) " +
+        "&& $(location aprotoc) " +
+        "  --plugin=$(location protoc-gen-javastream) " +
+        "  --dependency_out=$(depfile) " +
+        "  --javastream_out=$(genDir)/$(in) " +
+        "  -Iexternal/protobuf/src " +
+        "  -I . " +
+        "  $(in) " +
+        "&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
+
+    srcs: [
+        ":framework-connectivity-protos",
+    ],
+    output_extension: "srcjar",
+}
diff --git a/framework/aidl-export/android/net/NetworkStats.aidl b/framework/aidl-export/android/net/NetworkStats.aidl
new file mode 100644
index 0000000..d06ca65
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkStats.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable NetworkStats;
diff --git a/framework/aidl-export/android/net/NetworkTemplate.aidl b/framework/aidl-export/android/net/NetworkTemplate.aidl
new file mode 100644
index 0000000..3d37488
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkTemplate.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable NetworkTemplate;
diff --git a/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl b/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl
new file mode 100644
index 0000000..657bdd1
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+@JavaOnlyStableParcelable parcelable NsdServiceInfo;
\ No newline at end of file
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 5579db6..ddac19d 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -6,14 +6,18 @@
   }
 
   public class ConnectivityManager {
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void addUidToMeteredNetworkAllowList(int);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void addUidToMeteredNetworkDenyList(int);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void factoryReset();
     method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public java.util.List<android.net.NetworkStateSnapshot> getAllNetworkStateSnapshots();
     method @Nullable public android.net.ProxyInfo getGlobalProxy();
     method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
-    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties redactLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
-    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities redactNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
+    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties getRedactedLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
+    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkAllowList(int);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkDenyList(int);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void replaceFirewallChain(int, @NonNull int[]);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void requestBackgroundNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
     method @Deprecated public boolean requestRouteToHostAddress(int, java.net.InetAddress);
@@ -26,13 +30,10 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
     method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreferences(@NonNull android.os.UserHandle, @NonNull java.util.List<android.net.ProfileNetworkPreference>, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setRequireVpnForUids(boolean, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setUidFirewallRule(int, int, int);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_STACK}) public void simulateDataStall(int, long, @NonNull android.net.Network, @NonNull android.os.PersistableBundle);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void startCaptivePortalApp(@NonNull android.net.Network);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void swapActiveStatsMap();
     method public void systemReady();
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateFirewallRule(int, int, boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateMeteredNetworkAllowList(int, boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateMeteredNetworkDenyList(int, boolean);
     field public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE";
     field public static final String ACTION_PROMPT_LOST_VALIDATION = "android.net.action.PROMPT_LOST_VALIDATION";
     field public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY = "android.net.action.PROMPT_PARTIAL_CONNECTIVITY";
@@ -53,6 +54,9 @@
     field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3
     field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4
     field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2
+    field public static final int FIREWALL_RULE_ALLOW = 1; // 0x1
+    field public static final int FIREWALL_RULE_DEFAULT = 0; // 0x0
+    field public static final int FIREWALL_RULE_DENY = 2; // 0x2
     field public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; // 0x0
     field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; // 0x1
     field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK = 2; // 0x2
@@ -92,7 +96,7 @@
     method public static void setDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
     method public static void setDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, @IntRange(from=0, to=100) int);
     method public static void setGlobalProxy(@NonNull android.content.Context, @NonNull android.net.ProxyInfo);
-    method public static void setIngressRateLimitInBytesPerSecond(@NonNull android.content.Context, @IntRange(from=0xffffffff, to=java.lang.Integer.MAX_VALUE) long);
+    method public static void setIngressRateLimitInBytesPerSecond(@NonNull android.content.Context, @IntRange(from=-1L, to=4294967295L) long);
     method public static void setMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
     method public static void setMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
     method public static void setMobileDataPreferredUids(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
@@ -128,8 +132,8 @@
 
   public final class NetworkAgentConfig implements android.os.Parcelable {
     method @Nullable public String getSubscriberId();
-    method public boolean getVpnRequiresValidation();
     method public boolean isBypassableVpn();
+    method public boolean isVpnValidationRequired();
   }
 
   public static final class NetworkAgentConfig.Builder {
@@ -140,7 +144,7 @@
   }
 
   public final class NetworkCapabilities implements android.os.Parcelable {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public java.util.Set<java.lang.Integer> getAccessUids();
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public java.util.Set<java.lang.Integer> getAllowedUids();
     method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids();
     method public boolean hasForbiddenCapability(int);
     field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL
@@ -152,7 +156,7 @@
   }
 
   public static final class NetworkCapabilities.Builder {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAccessUids(@NonNull java.util.Set<java.lang.Integer>);
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAllowedUids(@NonNull java.util.Set<java.lang.Integer>);
     method @NonNull public android.net.NetworkCapabilities.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
   }
 
@@ -171,8 +175,8 @@
 
   public final class ProfileNetworkPreference implements android.os.Parcelable {
     method public int describeContents();
-    method @NonNull public java.util.List<java.lang.Integer> getExcludedUids();
-    method @NonNull public java.util.List<java.lang.Integer> getIncludedUids();
+    method @NonNull public int[] getExcludedUids();
+    method @NonNull public int[] getIncludedUids();
     method public int getPreference();
     method public int getPreferenceEnterpriseId();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -182,8 +186,8 @@
   public static final class ProfileNetworkPreference.Builder {
     ctor public ProfileNetworkPreference.Builder();
     method @NonNull public android.net.ProfileNetworkPreference build();
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@Nullable java.util.List<java.lang.Integer>);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@Nullable java.util.List<java.lang.Integer>);
+    method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@NonNull int[]);
+    method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@NonNull int[]);
     method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int);
     method @NonNull public android.net.ProfileNetworkPreference.Builder setPreferenceEnterpriseId(int);
   }
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 764cffa..db1d7e9 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -104,12 +104,6 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
     field public static final int PROTOCOL_ANY = -1; // 0xffffffff
     field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
-    field public static final int STATUS_DELETED = 4; // 0x4
-    field public static final int STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; // 0x3
-    field public static final int STATUS_POLICY_NOT_FOUND = 5; // 0x5
-    field public static final int STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; // 0x2
-    field public static final int STATUS_REQUEST_DECLINED = 1; // 0x1
-    field public static final int STATUS_SUCCESS = 0; // 0x0
   }
 
   public static final class DscpPolicy.Builder {
@@ -270,6 +264,13 @@
     method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
     method public final void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
     method public void unregister();
+    method public void unregisterAfterReplacement(@IntRange(from=0, to=0x1388) int);
+    field public static final int DSCP_POLICY_STATUS_DELETED = 4; // 0x4
+    field public static final int DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; // 0x3
+    field public static final int DSCP_POLICY_STATUS_POLICY_NOT_FOUND = 5; // 0x5
+    field public static final int DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; // 0x2
+    field public static final int DSCP_POLICY_STATUS_REQUEST_DECLINED = 1; // 0x1
+    field public static final int DSCP_POLICY_STATUS_SUCCESS = 0; // 0x0
     field public static final int VALIDATION_STATUS_NOT_VALID = 2; // 0x2
     field public static final int VALIDATION_STATUS_VALID = 1; // 0x1
   }
@@ -339,7 +340,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setSsid(@Nullable String);
     method @NonNull public android.net.NetworkCapabilities.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
     method @NonNull public android.net.NetworkCapabilities.Builder setTransportInfo(@Nullable android.net.TransportInfo);
-    method @NonNull public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
     method @NonNull public static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities();
   }
 
@@ -360,6 +361,7 @@
   }
 
   public class NetworkReleasedException extends java.lang.Exception {
+    ctor public NetworkReleasedException();
   }
 
   public class NetworkRequest implements android.os.Parcelable {
@@ -424,6 +426,8 @@
   }
 
   public final class QosCallbackException extends java.lang.Exception {
+    ctor public QosCallbackException(@NonNull String);
+    ctor public QosCallbackException(@NonNull Throwable);
   }
 
   public abstract class QosFilter {
@@ -469,9 +473,11 @@
   }
 
   public class SocketLocalAddressChangedException extends java.lang.Exception {
+    ctor public SocketLocalAddressChangedException();
   }
 
   public class SocketNotBoundException extends java.lang.Exception {
+    ctor public SocketNotBoundException();
   }
 
   public final class StaticIpConfiguration implements android.os.Parcelable {
diff --git a/framework/proto/netstats.proto b/framework/proto/netstats.proto
new file mode 100644
index 0000000..3c9f73c
--- /dev/null
+++ b/framework/proto/netstats.proto
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+package android.service;
+
+option java_multiple_files = true;
+option java_outer_classname = "NetworkStatsServiceProto";
+
+// Represents dumpsys from NetworkStatsService (netstats).
+message NetworkStatsServiceDumpProto {
+  repeated NetworkInterfaceProto active_interfaces = 1;
+
+  repeated NetworkInterfaceProto active_uid_interfaces = 2;
+
+  // Device level network stats, which may include non-IP layer traffic.
+  optional NetworkStatsRecorderProto dev_stats = 3;
+
+  // IP-layer traffic stats.
+  optional NetworkStatsRecorderProto xt_stats = 4;
+
+  // Per-UID network stats.
+  optional NetworkStatsRecorderProto uid_stats = 5;
+
+  // Per-UID, per-tag network stats, excluding the default tag (i.e. tag=0).
+  optional NetworkStatsRecorderProto uid_tag_stats = 6;
+}
+
+// Corresponds to NetworkStatsService.mActiveIfaces/mActiveUidIfaces.
+message NetworkInterfaceProto {
+  // Name of the network interface (eg: wlan).
+  optional string interface = 1;
+
+  optional NetworkIdentitySetProto identities = 2;
+}
+
+// Corresponds to NetworkIdentitySet.
+message NetworkIdentitySetProto {
+  repeated NetworkIdentityProto identities = 1;
+}
+
+// Corresponds to NetworkIdentity.
+message NetworkIdentityProto {
+  // Constants from ConnectivityManager.TYPE_*.
+  optional int32 type = 1;
+
+  optional bool roaming = 4;
+
+  optional bool metered = 5;
+
+  optional bool default_network = 6;
+
+  optional int32 oem_managed_network = 7;
+}
+
+// Corresponds to NetworkStatsRecorder.
+message NetworkStatsRecorderProto {
+  optional int64 pending_total_bytes = 1;
+
+  optional NetworkStatsCollectionProto complete_history = 2;
+}
+
+// Corresponds to NetworkStatsCollection.
+message NetworkStatsCollectionProto {
+  repeated NetworkStatsCollectionStatsProto stats = 1;
+}
+
+// Corresponds to NetworkStatsCollection.mStats.
+message NetworkStatsCollectionStatsProto {
+  optional NetworkStatsCollectionKeyProto key = 1;
+
+  optional NetworkStatsHistoryProto history = 2;
+}
+
+// Corresponds to NetworkStatsCollection.Key.
+message NetworkStatsCollectionKeyProto {
+  optional NetworkIdentitySetProto identity = 1;
+
+  optional int32 uid = 2;
+
+  optional int32 set = 3;
+
+  optional int32 tag = 4;
+}
+
+// Corresponds to NetworkStatsHistory.
+message NetworkStatsHistoryProto {
+  // Duration for this bucket in milliseconds.
+  optional int64 bucket_duration_ms = 1;
+
+  repeated NetworkStatsHistoryBucketProto buckets = 2;
+}
+
+// Corresponds to each bucket in NetworkStatsHistory.
+message NetworkStatsHistoryBucketProto {
+  // Bucket start time in milliseconds since epoch.
+  optional int64 bucket_start_ms = 1;
+
+  optional int64 rx_bytes = 2;
+
+  optional int64 rx_packets = 3;
+
+  optional int64 tx_bytes = 4;
+
+  optional int64 tx_packets = 5;
+
+  optional int64 operations = 6;
+}
\ No newline at end of file
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index e8e1efa..a174fe3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -995,6 +995,40 @@
     // LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)
 
     /**
+     * A firewall rule which allows or drops packets depending on existing policy.
+     * Used by {@link #setUidFirewallRule(int, int, int)} to follow existing policy to handle
+     * specific uid's packets in specific firewall chain.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_RULE_DEFAULT = 0;
+
+    /**
+     * A firewall rule which allows packets. Used by {@link #setUidFirewallRule(int, int, int)} to
+     * allow specific uid's packets in specific firewall chain.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_RULE_ALLOW = 1;
+
+    /**
+     * A firewall rule which drops packets. Used by {@link #setUidFirewallRule(int, int, int)} to
+     * drop specific uid's packets in specific firewall chain.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_RULE_DENY = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = false, prefix = "FIREWALL_RULE_", value = {
+        FIREWALL_RULE_DEFAULT,
+        FIREWALL_RULE_ALLOW,
+        FIREWALL_RULE_DENY
+    })
+    public @interface FirewallRule {}
+
+    /**
      * A kludge to facilitate static access where a Context pointer isn't available, like in the
      * case of the static set/getProcessDefaultNetwork methods and from the Network class.
      * TODO: Remove this after deprecating the static methods in favor of non-static methods or
@@ -1643,10 +1677,10 @@
             android.Manifest.permission.NETWORK_SETTINGS})
     @SystemApi(client = MODULE_LIBRARIES)
     @Nullable
-    public LinkProperties redactLinkPropertiesForPackage(@NonNull LinkProperties lp, int uid,
+    public LinkProperties getRedactedLinkPropertiesForPackage(@NonNull LinkProperties lp, int uid,
             @NonNull String packageName) {
         try {
-            return mService.redactLinkPropertiesForPackage(
+            return mService.getRedactedLinkPropertiesForPackage(
                     lp, uid, packageName, getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1683,9 +1717,11 @@
      * Redact {@link NetworkCapabilities} for a given package.
      *
      * Returns an instance of {@link NetworkCapabilities} that is appropriately redacted to send
-     * to the given package, considering its permissions. Calling this method will blame the UID for
-     * retrieving the device location if the passed capabilities contain location-sensitive
-     * information.
+     * to the given package, considering its permissions. If the passed capabilities contain
+     * location-sensitive information, they will be redacted to the correct degree for the location
+     * permissions of the app (COARSE or FINE), and will blame the UID accordingly for retrieving
+     * that level of location. If the UID holds no location permission, the returned object will
+     * contain no location-sensitive information and the UID is not blamed.
      *
      * @param nc A {@link NetworkCapabilities} instance which will be redacted.
      * @param uid The target uid.
@@ -1700,11 +1736,11 @@
             android.Manifest.permission.NETWORK_SETTINGS})
     @SystemApi(client = MODULE_LIBRARIES)
     @Nullable
-    public NetworkCapabilities redactNetworkCapabilitiesForPackage(
+    public NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(
             @NonNull NetworkCapabilities nc,
             int uid, @NonNull String packageName) {
         try {
-            return mService.redactNetworkCapabilitiesForPackage(nc, uid, packageName,
+            return mService.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName,
                     getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -5706,8 +5742,8 @@
     }
 
     /**
-     * Sets whether the specified UID is allowed to use data on metered networks even when
-     * background data is restricted.
+     * Adds the specified UID to the list of UIds that are allowed to use data on metered networks
+     * even when background data is restricted. The deny list takes precedence over the allow list.
      *
      * @param uid uid of target app
      * @throws IllegalStateException if updating allow list failed.
@@ -5719,17 +5755,40 @@
             android.Manifest.permission.NETWORK_STACK,
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
     })
-    public void updateMeteredNetworkAllowList(final int uid, final boolean add) {
+    public void addUidToMeteredNetworkAllowList(final int uid) {
         try {
-            mService.updateMeteredNetworkAllowList(uid, add);
+            mService.updateMeteredNetworkAllowList(uid, true /* add */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
     /**
-     * Sets whether the specified UID is prevented from using background data on metered networks.
-     * Takes precedence over {@link #updateMeteredNetworkAllowList}.
+     * Removes the specified UID from the list of UIDs that are allowed to use background data on
+     * metered networks when background data is restricted. The deny list takes precedence over
+     * the allow list.
+     *
+     * @param uid uid of target app
+     * @throws IllegalStateException if updating allow list failed.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public void removeUidFromMeteredNetworkAllowList(final int uid) {
+        try {
+            mService.updateMeteredNetworkAllowList(uid, false /* remove */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Adds the specified UID to the list of UIDs that are not allowed to use background data on
+     * metered networks. Takes precedence over {@link #addUidToMeteredNetworkAllowList}.
      *
      * @param uid uid of target app
      * @throws IllegalStateException if updating deny list failed.
@@ -5741,9 +5800,32 @@
             android.Manifest.permission.NETWORK_STACK,
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
     })
-    public void updateMeteredNetworkDenyList(final int uid, final boolean add) {
+    public void addUidToMeteredNetworkDenyList(final int uid) {
         try {
-            mService.updateMeteredNetworkDenyList(uid, add);
+            mService.updateMeteredNetworkDenyList(uid, true /* add */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Removes the specified UID from the list of UIds that can use use background data on metered
+     * networks if background data is not restricted. The deny list takes precedence over the
+     * allow list.
+     *
+     * @param uid uid of target app
+     * @throws IllegalStateException if updating deny list failed.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public void removeUidFromMeteredNetworkDenyList(final int uid) {
+        try {
+            mService.updateMeteredNetworkDenyList(uid, false /* remove */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -5754,8 +5836,9 @@
      *
      * @param chain target chain.
      * @param uid uid to allow/deny.
-     * @param allow whether networking is allowed or denied.
+     * @param rule firewall rule to allow/drop packets.
      * @throws IllegalStateException if updating firewall rule failed.
+     * @throws IllegalArgumentException if {@code rule} is not a valid rule.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
@@ -5764,10 +5847,10 @@
             android.Manifest.permission.NETWORK_STACK,
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
     })
-    public void updateFirewallRule(@FirewallChain final int chain, final int uid,
-            final boolean allow) {
+    public void setUidFirewallRule(@FirewallChain final int chain, final int uid,
+            @FirewallRule final int rule) {
         try {
-            mService.updateFirewallRule(chain, uid, allow);
+            mService.setUidFirewallRule(chain, uid, rule);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -5818,27 +5901,4 @@
             throw e.rethrowFromSystemServer();
         }
     }
-
-    /**
-     * Request to change the current active network stats map.
-     * STOPSHIP: Remove this API before T sdk finalized, this API is temporary added for the
-     * NetworkStatsFactory which is platform code but will be moved into connectivity (tethering)
-     * mainline module.
-     *
-     * @throws IllegalStateException if swapping active stats map failed.
-     * @hide
-     */
-    @SystemApi(client = MODULE_LIBRARIES)
-    @RequiresPermission(anyOf = {
-            android.Manifest.permission.NETWORK_SETTINGS,
-            android.Manifest.permission.NETWORK_STACK,
-            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
-    })
-    public void swapActiveStatsMap() {
-        try {
-            mService.swapActiveStatsMap();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
 }
diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java
index 4e28b29..822e67d 100644
--- a/framework/src/android/net/ConnectivitySettingsManager.java
+++ b/framework/src/android/net/ConnectivitySettingsManager.java
@@ -1081,10 +1081,10 @@
     }
 
     /**
-     * Get the global network bandwidth rate limit.
+     * Get the network bandwidth ingress rate limit.
      *
-     * The limit is only applicable to networks that provide internet connectivity. If the setting
-     * is unset, it defaults to -1.
+     * The limit is only applicable to networks that provide internet connectivity. -1 codes for no
+     * bandwidth limitation.
      *
      * @param context The {@link Context} to query the setting.
      * @return The rate limit in number of bytes per second or -1 if disabled.
@@ -1095,15 +1095,17 @@
     }
 
     /**
-     * Set the global network bandwidth rate limit.
+     * Set the network bandwidth ingress rate limit.
      *
-     * The limit is only applicable to networks that provide internet connectivity.
+     * The limit is applied to all networks that provide internet connectivity. It is applied on a
+     * per-network basis, meaning that global ingress rate could exceed the limit when communicating
+     * on multiple networks simultaneously.
      *
      * @param context The {@link Context} to set the setting.
      * @param rateLimitInBytesPerSec The rate limit in number of bytes per second or -1 to disable.
      */
     public static void setIngressRateLimitInBytesPerSecond(@NonNull Context context,
-            @IntRange(from = -1, to = Integer.MAX_VALUE) long rateLimitInBytesPerSec) {
+            @IntRange(from = -1L, to = 0xFFFFFFFFL) long rateLimitInBytesPerSec) {
         if (rateLimitInBytesPerSec < -1) {
             throw new IllegalArgumentException(
                     "Rate limit must be within the range [-1, Integer.MAX_VALUE]");
diff --git a/framework/src/android/net/DscpPolicy.java b/framework/src/android/net/DscpPolicy.java
index cda8205..6af795b 100644
--- a/framework/src/android/net/DscpPolicy.java
+++ b/framework/src/android/net/DscpPolicy.java
@@ -16,7 +16,6 @@
 
 package android.net;
 
-import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -26,8 +25,6 @@
 
 import com.android.net.module.util.InetAddressUtils;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.util.Objects;
@@ -49,36 +46,6 @@
      */
     public static final int SOURCE_PORT_ANY = -1;
 
-    /**
-     * Policy was successfully added.
-     */
-    public static final int STATUS_SUCCESS = 0;
-
-    /**
-     * Policy was rejected for any reason besides invalid classifier or insufficient resources.
-     */
-    public static final int STATUS_REQUEST_DECLINED = 1;
-
-    /**
-     * Requested policy contained a classifier which is not supported.
-     */
-    public static final int STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2;
-
-    /**
-     * TODO: should this error case be supported?
-     */
-    public static final int STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3;
-
-    /**
-     * Policy was deleted.
-     */
-    public static final int STATUS_DELETED = 4;
-
-    /**
-     * Policy was not found during deletion.
-     */
-    public static final int STATUS_POLICY_NOT_FOUND = 5;
-
     /** The unique policy ID. Each requesting network is responsible for maintaining policy IDs
      * unique within that network. In the case where a policy with an existing ID is created, the
      * new policy will update the existing policy with the same ID.
@@ -112,17 +79,6 @@
         return 0;
     }
 
-    /** @hide */
-    @IntDef(prefix = "STATUS_", value = {
-        STATUS_SUCCESS,
-        STATUS_REQUEST_DECLINED,
-        STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
-        STATUS_INSUFFICIENT_PROCESSING_RESOURCES,
-        STATUS_DELETED
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface DscpPolicyStatus {}
-
     /* package */ DscpPolicy(
             int policyId,
             int dscp,
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 23a3850..bc73769 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -76,13 +76,13 @@
     LinkProperties getActiveLinkProperties();
     LinkProperties getLinkPropertiesForType(int networkType);
     LinkProperties getLinkProperties(in Network network);
-    LinkProperties redactLinkPropertiesForPackage(in LinkProperties lp, int uid, String packageName,
-            String callingAttributionTag);
+    LinkProperties getRedactedLinkPropertiesForPackage(in LinkProperties lp, int uid,
+            String packageName, String callingAttributionTag);
 
     NetworkCapabilities getNetworkCapabilities(in Network network, String callingPackageName,
             String callingAttributionTag);
 
-    NetworkCapabilities redactNetworkCapabilitiesForPackage(in NetworkCapabilities nc, int uid,
+    NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(in NetworkCapabilities nc, int uid,
             String callingPackageName, String callingAttributionTag);
 
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
@@ -240,11 +240,9 @@
 
     void updateMeteredNetworkDenyList(int uid, boolean add);
 
-    void updateFirewallRule(int chain, int uid, boolean allow);
+    void setUidFirewallRule(int chain, int uid, int rule);
 
     void setFirewallChainEnabled(int chain, boolean enable);
 
     void replaceFirewallChain(int chain, in int[] uids);
-
-    void swapActiveStatsMap();
 }
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index 08536ca..b375b7b 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -47,4 +47,5 @@
     void sendAddDscpPolicy(in DscpPolicy policy);
     void sendRemoveDscpPolicy(int policyId);
     void sendRemoveAllDscpPolicies();
+    void sendUnregisterAfterReplacement(int timeoutMillis);
 }
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
index 2a863ad..847f14e 100644
--- a/framework/src/android/net/ITestNetworkManager.aidl
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -29,8 +29,7 @@
  */
 interface ITestNetworkManager
 {
-    TestNetworkInterface createTunInterface(in LinkAddress[] linkAddrs);
-    TestNetworkInterface createTapInterface();
+    TestNetworkInterface createInterface(boolean isTun, boolean bringUp, in LinkAddress[] addrs);
 
     void setupTestNetwork(in String iface, in LinkProperties lp, in boolean isMetered,
             in int[] administratorUids, in IBinder binder);
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
index 99f48b4..8782b33 100644
--- a/framework/src/android/net/LinkProperties.java
+++ b/framework/src/android/net/LinkProperties.java
@@ -19,12 +19,16 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.LinkPropertiesUtils;
 
 import java.net.Inet4Address;
@@ -38,6 +42,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.StringJoiner;
+import java.util.stream.Collectors;
 
 /**
  * Describes the properties of a network link.
@@ -52,6 +57,17 @@
  *
  */
 public final class LinkProperties implements Parcelable {
+    /**
+     * The {@link #getRoutes()} now can contain excluded as well as included routes. Use
+     * {@link RouteInfo#getType()} to determine route type.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S) // Switch to S_V2 when it is available.
+    @VisibleForTesting
+    public static final long EXCLUDED_ROUTES = 186082280;
+
     // The interface described by the network link.
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private String mIfaceName;
@@ -738,10 +754,25 @@
     /**
      * Returns all the {@link RouteInfo} set on this link.
      *
+     * Only unicast routes are returned for apps targeting Android S or below.
+     *
      * @return An unmodifiable {@link List} of {@link RouteInfo} for this link.
      */
     public @NonNull List<RouteInfo> getRoutes() {
-        return Collections.unmodifiableList(mRoutes);
+        if (CompatChanges.isChangeEnabled(EXCLUDED_ROUTES)) {
+            return Collections.unmodifiableList(mRoutes);
+        } else {
+            return Collections.unmodifiableList(getUnicastRoutes());
+        }
+    }
+
+    /**
+     * Returns all the {@link RouteInfo} of type {@link RouteInfo#RTN_UNICAST} set on this link.
+     */
+    private @NonNull List<RouteInfo> getUnicastRoutes() {
+        return mRoutes.stream()
+                .filter(route -> route.getType() == RouteInfo.RTN_UNICAST)
+                .collect(Collectors.toList());
     }
 
     /**
@@ -757,11 +788,14 @@
 
     /**
      * Returns all the routes on this link and all the links stacked above it.
+     *
+     * Only unicast routes are returned for apps targeting Android S or below.
+     *
      * @hide
      */
     @SystemApi
     public @NonNull List<RouteInfo> getAllRoutes() {
-        List<RouteInfo> routes = new ArrayList<>(mRoutes);
+        final List<RouteInfo> routes = new ArrayList<>(getRoutes());
         for (LinkProperties stacked: mStackedLinks.values()) {
             routes.addAll(stacked.getAllRoutes());
         }
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 945e670..29add1c 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -25,7 +25,6 @@
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
-import android.net.DscpPolicy.DscpPolicyStatus;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.ConditionVariable;
@@ -434,6 +433,56 @@
      */
     public static final int CMD_DSCP_POLICY_STATUS = BASE + 28;
 
+    /**
+     * DSCP policy was successfully added.
+     */
+    public static final int DSCP_POLICY_STATUS_SUCCESS = 0;
+
+    /**
+     * DSCP policy was rejected for any reason besides invalid classifier or insufficient resources.
+     */
+    public static final int DSCP_POLICY_STATUS_REQUEST_DECLINED = 1;
+
+    /**
+     * Requested DSCP policy contained a classifier which is not supported.
+     */
+    public static final int DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2;
+
+    /**
+     * Requested DSCP policy was not added due to insufficient processing resources.
+     */
+    // TODO: should this error case be supported?
+    public static final int DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3;
+
+    /**
+     * DSCP policy was deleted.
+     */
+    public static final int DSCP_POLICY_STATUS_DELETED = 4;
+
+    /**
+     * DSCP policy was not found during deletion.
+     */
+    public static final int DSCP_POLICY_STATUS_POLICY_NOT_FOUND = 5;
+
+    /** @hide */
+    @IntDef(prefix = "DSCP_POLICY_STATUS_", value = {
+        DSCP_POLICY_STATUS_SUCCESS,
+        DSCP_POLICY_STATUS_REQUEST_DECLINED,
+        DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
+        DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES,
+        DSCP_POLICY_STATUS_DELETED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DscpPolicyStatus {}
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to notify that this network is expected to be
+     * replaced within the specified time by a similar network.
+     * arg1 = timeout in milliseconds
+     * @hide
+     */
+    public static final int EVENT_UNREGISTER_AFTER_REPLACEMENT = BASE + 29;
+
     private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
         final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
                 config.legacyTypeName, config.legacySubTypeName);
@@ -943,6 +992,45 @@
     }
 
     /**
+     * Indicates that this agent will likely soon be replaced by another agent for a very similar
+     * network (e.g., same Wi-Fi SSID).
+     *
+     * If the network is not currently satisfying any {@link NetworkRequest}s, it will be torn down.
+     * If it is satisfying requests, then the native network corresponding to the agent will be
+     * destroyed immediately, but the agent will remain registered and will continue to satisfy
+     * requests until {@link #unregister} is called, the network is replaced by an equivalent or
+     * better network, or the specified timeout expires. During this time:
+     *
+     * <ul>
+     * <li>The agent may not send any further updates, for example by calling methods
+     *    such as {@link #sendNetworkCapabilities}, {@link #sendLinkProperties},
+     *    {@link #sendNetworkScore(NetworkScore)} and so on. Any such updates will be ignored.
+     * <li>The network will remain connected and continue to satisfy any requests that it would
+     *    otherwise satisfy (including, possibly, the default request).
+     * <li>The validation state of the network will not change, and calls to
+     *    {@link ConnectivityManager#reportNetworkConnectivity(Network, boolean)} will be ignored.
+     * </ul>
+     *
+     * Once this method is called, it is not possible to restore the agent to a functioning state.
+     * If a replacement network becomes available, then a new agent must be registered. When that
+     * replacement network is fully capable of replacing this network (including, possibly, being
+     * validated), this agent will no longer be needed and will be torn down. Otherwise, this agent
+     * can be disconnected by calling {@link #unregister}. If {@link #unregister} is not called,
+     * this agent will automatically be unregistered when the specified timeout expires. Any
+     * teardown delay previously set using{@link #setTeardownDelayMillis} is ignored.
+     *
+     * <p>This method has no effect if {@link #markConnected} has not yet been called.
+     * <p>This method may only be called once.
+     *
+     * @param timeoutMillis the timeout after which this network will be unregistered even if
+     *                      {@link #unregister} was not called.
+     */
+    public void unregisterAfterReplacement(
+            @IntRange(from = 0, to = MAX_TEARDOWN_DELAY_MS) int timeoutMillis) {
+        queueOrSendMessage(reg -> reg.sendUnregisterAfterReplacement(timeoutMillis));
+    }
+
+    /**
      * Change the legacy subtype of this network agent.
      *
      * This is only for backward compatibility and should not be used by non-legacy network agents,
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index 1991a58..0d2b620 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -24,6 +24,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.Objects;
 
 /**
@@ -250,7 +252,7 @@
 
     /**
      * Whether network validation should be performed for this VPN network.
-     * {@see #getVpnRequiresValidation}
+     * {@see #isVpnValidationRequired}
      * @hide
      */
     private boolean mVpnRequiresValidation = false;
@@ -265,7 +267,7 @@
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
-    public boolean getVpnRequiresValidation() {
+    public boolean isVpnValidationRequired() {
         return mVpnRequiresValidation;
     }
 
@@ -473,6 +475,9 @@
         @NonNull
         @SystemApi(client = MODULE_LIBRARIES)
         public Builder setLocalRoutesExcludedForVpn(boolean excludeLocalRoutes) {
+            if (!SdkLevel.isAtLeastT()) {
+                throw new UnsupportedOperationException("Method is not supported");
+            }
             mConfig.excludeLocalRouteVpn = excludeLocalRoutes;
             return this;
         }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 41be732..97b1f32 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -147,27 +147,32 @@
     private String mRequestorPackageName;
 
     /**
-     * Enterprise capability identifier 1.
+     * Enterprise capability identifier 1. It will be used to uniquely identify specific
+     * enterprise network.
      */
     public static final int NET_ENTERPRISE_ID_1 = 1;
 
     /**
-     * Enterprise capability identifier 2.
+     * Enterprise capability identifier 2. It will be used to uniquely identify specific
+     * enterprise network.
      */
     public static final int NET_ENTERPRISE_ID_2 = 2;
 
     /**
-     * Enterprise capability identifier 3.
+     * Enterprise capability identifier 3. It will be used to uniquely identify specific
+     * enterprise network.
      */
     public static final int NET_ENTERPRISE_ID_3 = 3;
 
     /**
-     * Enterprise capability identifier 4.
+     * Enterprise capability identifier 4. It will be used to uniquely identify specific
+     * enterprise network.
      */
     public static final int NET_ENTERPRISE_ID_4 = 4;
 
     /**
-     * Enterprise capability identifier 5.
+     * Enterprise capability identifier 5. It will be used to uniquely identify specific
+     * enterprise network.
      */
     public static final int NET_ENTERPRISE_ID_5 = 5;
 
@@ -264,7 +269,7 @@
         mTransportInfo = null;
         mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED;
         mUids = null;
-        mAccessUids.clear();
+        mAllowedUids.clear();
         mAdministratorUids = new int[0];
         mOwnerUid = Process.INVALID_UID;
         mSSID = null;
@@ -295,7 +300,7 @@
         }
         mSignalStrength = nc.mSignalStrength;
         mUids = (nc.mUids == null) ? null : new ArraySet<>(nc.mUids);
-        setAccessUids(nc.mAccessUids);
+        setAllowedUids(nc.mAllowedUids);
         setAdministratorUids(nc.getAdministratorUids());
         mOwnerUid = nc.mOwnerUid;
         mForbiddenNetworkCapabilities = nc.mForbiddenNetworkCapabilities;
@@ -858,8 +863,11 @@
     }
 
     /**
-     * Get the underlying networks of this network. If the caller is not system privileged, this is
-     * always redacted to null and it will be never useful to the caller.
+     * Get the underlying networks of this network. If the caller doesn't have one of
+     * {@link android.Manifest.permission.NETWORK_FACTORY},
+     * {@link android.Manifest.permission.NETWORK_SETTINGS} and
+     * {@link NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}, this is always redacted to null and
+     * it will be never useful to the caller.
      *
      * @return <li>If the list is null, this network hasn't declared underlying networks.</li>
      *         <li>If the list is empty, this network has declared that it has no underlying
@@ -1029,7 +1037,7 @@
         final int[] originalAdministratorUids = getAdministratorUids();
         final TransportInfo originalTransportInfo = getTransportInfo();
         final Set<Integer> originalSubIds = getSubscriptionIds();
-        final Set<Integer> originalAccessUids = new ArraySet<>(mAccessUids);
+        final Set<Integer> originalAllowedUids = new ArraySet<>(mAllowedUids);
         clearAll();
         if (0 != (originalCapabilities & (1 << NET_CAPABILITY_NOT_RESTRICTED))) {
             // If the test network is not restricted, then it is only allowed to declare some
@@ -1049,7 +1057,7 @@
         mNetworkSpecifier = originalSpecifier;
         mSignalStrength = originalSignalStrength;
         mTransportInfo = originalTransportInfo;
-        mAccessUids.addAll(originalAccessUids);
+        mAllowedUids.addAll(originalAllowedUids);
 
         // Only retain the owner and administrator UIDs if they match the app registering the remote
         // caller that registered the network.
@@ -1836,20 +1844,20 @@
      * @hide
      */
     @NonNull
-    private final ArraySet<Integer> mAccessUids = new ArraySet<>();
+    private final ArraySet<Integer> mAllowedUids = new ArraySet<>();
 
     /**
      * Set the list of UIDs that can always access this network.
      * @param uids
      * @hide
      */
-    public void setAccessUids(@NonNull final Set<Integer> uids) {
+    public void setAllowedUids(@NonNull final Set<Integer> uids) {
         // could happen with nc.set(nc), cheaper than always making a defensive copy
-        if (uids == mAccessUids) return;
+        if (uids == mAllowedUids) return;
 
         Objects.requireNonNull(uids);
-        mAccessUids.clear();
-        mAccessUids.addAll(uids);
+        mAllowedUids.clear();
+        mAllowedUids.addAll(uids);
     }
 
     /**
@@ -1867,35 +1875,36 @@
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
-    public @NonNull Set<Integer> getAccessUids() {
-        return new ArraySet<>(mAccessUids);
+    public @NonNull Set<Integer> getAllowedUids() {
+        return new ArraySet<>(mAllowedUids);
     }
 
     /** @hide */
     // For internal clients that know what they are doing and need to avoid the performance hit
     // of the defensive copy.
-    public @NonNull ArraySet<Integer> getAccessUidsNoCopy() {
-        return mAccessUids;
+    public @NonNull ArraySet<Integer> getAllowedUidsNoCopy() {
+        return mAllowedUids;
     }
 
     /**
-     * Test whether this UID has special permission to access this network, as per mAccessUids.
+     * Test whether this UID has special permission to access this network, as per mAllowedUids.
      * @hide
      */
-    public boolean isAccessUid(int uid) {
-        return mAccessUids.contains(uid);
+    // TODO : should this be "doesUidHaveAccess" and check the USE_RESTRICTED_NETWORKS permission ?
+    public boolean isUidWithAccess(int uid) {
+        return mAllowedUids.contains(uid);
     }
 
     /**
      * @return whether any UID is in the list of access UIDs
      * @hide
      */
-    public boolean hasAccessUids() {
-        return !mAccessUids.isEmpty();
+    public boolean hasAllowedUids() {
+        return !mAllowedUids.isEmpty();
     }
 
-    private boolean equalsAccessUids(@NonNull NetworkCapabilities other) {
-        return mAccessUids.equals(other.mAccessUids);
+    private boolean equalsAllowedUids(@NonNull NetworkCapabilities other) {
+        return mAllowedUids.equals(other.mAllowedUids);
     }
 
     /**
@@ -2052,7 +2061,7 @@
                 && equalsSpecifier(that)
                 && equalsTransportInfo(that)
                 && equalsUids(that)
-                && equalsAccessUids(that)
+                && equalsAllowedUids(that)
                 && equalsSSID(that)
                 && equalsOwnerUid(that)
                 && equalsPrivateDnsBroken(that)
@@ -2077,7 +2086,7 @@
                 + mSignalStrength * 29
                 + mOwnerUid * 31
                 + Objects.hashCode(mUids) * 37
-                + Objects.hashCode(mAccessUids) * 41
+                + Objects.hashCode(mAllowedUids) * 41
                 + Objects.hashCode(mSSID) * 43
                 + Objects.hashCode(mTransportInfo) * 47
                 + Objects.hashCode(mPrivateDnsBroken) * 53
@@ -2114,7 +2123,7 @@
         dest.writeParcelable((Parcelable) mTransportInfo, flags);
         dest.writeInt(mSignalStrength);
         writeParcelableArraySet(dest, mUids, flags);
-        dest.writeIntArray(CollectionUtils.toIntArray(mAccessUids));
+        dest.writeIntArray(CollectionUtils.toIntArray(mAllowedUids));
         dest.writeString(mSSID);
         dest.writeBoolean(mPrivateDnsBroken);
         dest.writeIntArray(getAdministratorUids());
@@ -2141,10 +2150,10 @@
                 netCap.mTransportInfo = in.readParcelable(null);
                 netCap.mSignalStrength = in.readInt();
                 netCap.mUids = readParcelableArraySet(in, null /* ClassLoader, null for default */);
-                final int[] accessUids = in.createIntArray();
-                netCap.mAccessUids.ensureCapacity(accessUids.length);
-                for (int uid : accessUids) {
-                    netCap.mAccessUids.add(uid);
+                final int[] allowedUids = in.createIntArray();
+                netCap.mAllowedUids.ensureCapacity(allowedUids.length);
+                for (int uid : allowedUids) {
+                    netCap.mAllowedUids.add(uid);
                 }
                 netCap.mSSID = in.readString();
                 netCap.mPrivateDnsBroken = in.readBoolean();
@@ -2223,8 +2232,8 @@
             }
         }
 
-        if (hasAccessUids()) {
-            sb.append(" AccessUids: <").append(mAccessUids).append(">");
+        if (hasAllowedUids()) {
+            sb.append(" AllowedUids: <").append(mAllowedUids).append(">");
         }
 
         if (mOwnerUid != Process.INVALID_UID) {
@@ -2644,7 +2653,7 @@
     /**
      * Builder class for NetworkCapabilities.
      *
-     * This class is mainly for for {@link NetworkAgent} instances to use. Many fields in
+     * This class is mainly for {@link NetworkAgent} instances to use. Many fields in
      * the built class require holding a signature permission to use - mostly
      * {@link android.Manifest.permission.NETWORK_FACTORY}, but refer to the specific
      * description of each setter. As this class lives entirely in app space it does not
@@ -3043,18 +3052,29 @@
         @NonNull
         @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
-        public Builder setAccessUids(@NonNull Set<Integer> uids) {
+        public Builder setAllowedUids(@NonNull Set<Integer> uids) {
             Objects.requireNonNull(uids);
-            mCaps.setAccessUids(uids);
+            mCaps.setAllowedUids(uids);
             return this;
         }
 
         /**
          * Set the underlying networks of this network.
          *
+         * <p>This API is mainly for {@link NetworkAgent}s who hold
+         * {@link android.Manifest.permission.NETWORK_FACTORY} to set its underlying networks.
+         *
+         * <p>The underlying networks are only visible for the receiver who has one of
+         * {@link android.Manifest.permission.NETWORK_FACTORY},
+         * {@link android.Manifest.permission.NETWORK_SETTINGS} and
+         * {@link NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}.
+         * If the receiver doesn't have required permissions, the field will be cleared before
+         * sending to the caller.</p>
+         *
          * @param networks The underlying networks of this network.
          */
         @NonNull
+        @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
         public Builder setUnderlyingNetworks(@Nullable List<Network> networks) {
             mCaps.setUnderlyingNetworks(networks);
             return this;
diff --git a/framework/src/android/net/NetworkReleasedException.java b/framework/src/android/net/NetworkReleasedException.java
index 0629b75..cdfb6a1 100644
--- a/framework/src/android/net/NetworkReleasedException.java
+++ b/framework/src/android/net/NetworkReleasedException.java
@@ -18,6 +18,8 @@
 
 import android.annotation.SystemApi;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * Indicates that the {@link Network} was released and is no longer available.
  *
@@ -25,7 +27,7 @@
  */
 @SystemApi
 public class NetworkReleasedException extends Exception {
-    /** @hide */
+    @VisibleForTesting
     public NetworkReleasedException() {
         super("The network was released and is no longer available");
     }
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index f43acce..fb271e3 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -22,14 +22,12 @@
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.net.ConnectivityManager.ProfileNetworkPreferencePolicy;
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Arrays;
 import java.util.Objects;
 
 /**
@@ -41,31 +39,31 @@
 public final class ProfileNetworkPreference implements Parcelable {
     private final @ProfileNetworkPreferencePolicy int mPreference;
     private final @NetworkCapabilities.EnterpriseId int mPreferenceEnterpriseId;
-    private final List<Integer> mIncludedUids;
-    private final List<Integer> mExcludedUids;
+    private int[] mIncludedUids = new int[0];
+    private int[] mExcludedUids = new int[0];
 
-    private ProfileNetworkPreference(int preference, List<Integer> includedUids,
-            List<Integer> excludedUids,
+    private ProfileNetworkPreference(int preference, int[] includedUids,
+            int[] excludedUids,
             @NetworkCapabilities.EnterpriseId int preferenceEnterpriseId) {
         mPreference = preference;
         mPreferenceEnterpriseId = preferenceEnterpriseId;
         if (includedUids != null) {
-            mIncludedUids = new ArrayList<>(includedUids);
+            mIncludedUids = includedUids.clone();
         } else {
-            mIncludedUids = new ArrayList<>();
+            mIncludedUids = new int[0];
         }
 
         if (excludedUids != null) {
-            mExcludedUids = new ArrayList<>(excludedUids);
+            mExcludedUids = excludedUids.clone();
         } else {
-            mExcludedUids = new ArrayList<>();
+            mExcludedUids = new int[0];
         }
     }
 
     private ProfileNetworkPreference(Parcel in) {
         mPreference = in.readInt();
-        mIncludedUids = in.readArrayList(Integer.class.getClassLoader());
-        mExcludedUids = in.readArrayList(Integer.class.getClassLoader());
+        in.readIntArray(mIncludedUids);
+        in.readIntArray(mExcludedUids);
         mPreferenceEnterpriseId = in.readInt();
     }
 
@@ -74,31 +72,31 @@
     }
 
     /**
-     * Get the list of UIDs subject to this preference.
+     * Get the array of UIDs subject to this preference.
      *
      * Included UIDs and Excluded UIDs can't both be non-empty.
      * if both are empty, it means this request applies to all uids in the user profile.
      * if included is not empty, then only included UIDs are applied.
      * if excluded is not empty, then it is all uids in the user profile except these UIDs.
-     * @return List of uids included for the profile preference.
+     * @return Array of uids included for the profile preference.
      * {@see #getExcludedUids()}
      */
-    public @NonNull List<Integer> getIncludedUids() {
-        return new ArrayList<>(mIncludedUids);
+    public @NonNull int[] getIncludedUids() {
+        return mIncludedUids.clone();
     }
 
     /**
-     * Get the list of UIDS excluded from this preference.
+     * Get the array of UIDS excluded from this preference.
      *
      * <ul>Included UIDs and Excluded UIDs can't both be non-empty.</ul>
      * <ul>If both are empty, it means this request applies to all uids in the user profile.</ul>
      * <ul>If included is not empty, then only included UIDs are applied.</ul>
      * <ul>If excluded is not empty, then it is all uids in the user profile except these UIDs.</ul>
-     * @return List of uids not included for the profile preference.
+     * @return Array of uids not included for the profile preference.
      * {@see #getIncludedUids()}
      */
-    public @NonNull List<Integer> getExcludedUids() {
-        return new ArrayList<>(mExcludedUids);
+    public @NonNull int[] getExcludedUids() {
+        return mExcludedUids.clone();
     }
 
     /**
@@ -134,8 +132,8 @@
         if (o == null || getClass() != o.getClass()) return false;
         final ProfileNetworkPreference that = (ProfileNetworkPreference) o;
         return mPreference == that.mPreference
-                && (Objects.equals(mIncludedUids, that.mIncludedUids))
-                && (Objects.equals(mExcludedUids, that.mExcludedUids))
+                && (Arrays.equals(mIncludedUids, that.mIncludedUids))
+                && (Arrays.equals(mExcludedUids, that.mExcludedUids))
                 && mPreferenceEnterpriseId == that.mPreferenceEnterpriseId;
     }
 
@@ -143,8 +141,8 @@
     public int hashCode() {
         return mPreference
                 + mPreferenceEnterpriseId * 2
-                + (Objects.hashCode(mIncludedUids) * 11)
-                + (Objects.hashCode(mExcludedUids) * 13);
+                + (Arrays.hashCode(mIncludedUids) * 11)
+                + (Arrays.hashCode(mExcludedUids) * 13);
     }
 
     /**
@@ -154,8 +152,8 @@
     public static final class Builder {
         private @ProfileNetworkPreferencePolicy int mPreference =
                 PROFILE_NETWORK_PREFERENCE_DEFAULT;
-        private @NonNull List<Integer> mIncludedUids = new ArrayList<>();
-        private @NonNull List<Integer> mExcludedUids = new ArrayList<>();
+        private int[] mIncludedUids = new int[0];
+        private int[] mExcludedUids = new int[0];
         private int mPreferenceEnterpriseId;
 
         /**
@@ -177,44 +175,38 @@
         }
 
         /**
-         * This is a list of uids for which profile perefence is set.
-         * Null would mean that this preference applies to all uids in the profile.
-         * {@see #setExcludedUids(List<Integer>)}
+         * This is a array of uids for which profile perefence is set.
+         * Empty would mean that this preference applies to all uids in the profile.
+         * {@see #setExcludedUids(int[])}
          * Included UIDs and Excluded UIDs can't both be non-empty.
          * if both are empty, it means this request applies to all uids in the user profile.
          * if included is not empty, then only included UIDs are applied.
          * if excluded is not empty, then it is all uids in the user profile except these UIDs.
-         * @param uids  list of uids that are included
+         * @param uids  Array of uids that are included
          * @return The builder to facilitate chaining.
          */
         @NonNull
-        public Builder setIncludedUids(@Nullable List<Integer> uids) {
-            if (uids != null) {
-                mIncludedUids = new ArrayList<Integer>(uids);
-            } else {
-                mIncludedUids = new ArrayList<Integer>();
-            }
+        public Builder setIncludedUids(@NonNull int[] uids) {
+            Objects.requireNonNull(uids);
+            mIncludedUids = uids.clone();
             return this;
         }
 
 
         /**
-         * This is a list of uids that are excluded for the profile perefence.
-         * {@see #setIncludedUids(List<Integer>)}
+         * This is a array of uids that are excluded for the profile perefence.
+         * {@see #setIncludedUids(int[])}
          * Included UIDs and Excluded UIDs can't both be non-empty.
          * if both are empty, it means this request applies to all uids in the user profile.
          * if included is not empty, then only included UIDs are applied.
          * if excluded is not empty, then it is all uids in the user profile except these UIDs.
-         * @param uids  list of uids that are not included
+         * @param uids  Array of uids that are not included
          * @return The builder to facilitate chaining.
          */
         @NonNull
-        public Builder setExcludedUids(@Nullable List<Integer> uids) {
-            if (uids != null) {
-                mExcludedUids = new ArrayList<Integer>(uids);
-            } else {
-                mExcludedUids = new ArrayList<Integer>();
-            }
+        public Builder setExcludedUids(@NonNull int[] uids) {
+            Objects.requireNonNull(uids);
+            mExcludedUids = uids.clone();
             return this;
         }
 
@@ -241,7 +233,7 @@
          */
         @NonNull
         public ProfileNetworkPreference  build() {
-            if (mIncludedUids.size() > 0 && mExcludedUids.size() > 0) {
+            if (mIncludedUids.length > 0 && mExcludedUids.length > 0) {
                 throw new IllegalArgumentException("Both includedUids and excludedUids "
                         + "cannot be nonempty");
             }
@@ -280,8 +272,8 @@
     @Override
     public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
         dest.writeInt(mPreference);
-        dest.writeList(mIncludedUids);
-        dest.writeList(mExcludedUids);
+        dest.writeIntArray(mIncludedUids);
+        dest.writeIntArray(mExcludedUids);
         dest.writeInt(mPreferenceEnterpriseId);
     }
 
diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java
index 7fd9a52..ed6eb15 100644
--- a/framework/src/android/net/QosCallbackException.java
+++ b/framework/src/android/net/QosCallbackException.java
@@ -21,6 +21,8 @@
 import android.annotation.SystemApi;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -94,16 +96,12 @@
         }
     }
 
-    /**
-     * @hide
-     */
+    @VisibleForTesting
     public QosCallbackException(@NonNull final String message) {
         super(message);
     }
 
-    /**
-     * @hide
-     */
+    @VisibleForTesting
     public QosCallbackException(@NonNull final Throwable cause) {
         super(cause);
     }
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
index 957c867..5c1c3cc 100644
--- a/framework/src/android/net/QosFilter.java
+++ b/framework/src/android/net/QosFilter.java
@@ -62,23 +62,31 @@
     public abstract int validate();
 
     /**
-     * Determines whether or not the parameters is a match for the filter.
+     * Determines whether or not the parameters will be matched with source address and port of this
+     * filter.
      *
-     * @param address the local address
-     * @param startPort the start of the port range
-     * @param endPort the end of the port range
-     * @return whether the parameters match the local address of the filter
+     * @param address the UE side address included in IP packet filter set of a QoS flow assigned
+     *                on {@link Network}.
+     * @param startPort the start of UE side port range included in IP packet filter set of a QoS
+     *                flow assigned on {@link Network}.
+     * @param endPort the end of UE side port range included in IP packet filter set of a QoS flow
+     *                assigned on {@link Network}.
+     * @return whether the parameters match the UE side address and port of the filter
      */
     public abstract boolean matchesLocalAddress(@NonNull InetAddress address,
             int startPort, int endPort);
 
     /**
-     * Determines whether or not the parameters is a match for the filter.
+     * Determines whether or not the parameters will be matched with remote address and port of
+     * this filter.
      *
-     * @param address the remote address
-     * @param startPort the start of the port range
-     * @param endPort the end of the port range
-     * @return whether the parameters match the remote address of the filter
+     * @param address the remote address included in IP packet filter set of a QoS flow
+     *                assigned on {@link Network}.
+     * @param startPort the start of remote port range included in IP packet filter set of a
+     *                 QoS flow assigned on {@link Network}.
+     * @param endPort the end of the remote range included in IP packet filter set of a QoS
+     *                flow assigned on {@link Network}.
+     * @return whether the parameters match the remote address and port of the filter
      */
     public abstract boolean matchesRemoteAddress(@NonNull InetAddress address,
             int startPort, int endPort);
diff --git a/framework/src/android/net/QosSession.java b/framework/src/android/net/QosSession.java
index 93f2ff2..25f3965 100644
--- a/framework/src/android/net/QosSession.java
+++ b/framework/src/android/net/QosSession.java
@@ -58,12 +58,12 @@
     }
 
     /**
-     * Gets the session id that is unique within that type.
+     * Gets the {@link QosSession} identifier which is set by the actor providing the QoS.
      * <p/>
-     * Note: The session id is set by the actor providing the qos.  It can be either manufactured by
-     * the actor, but also may have a particular meaning within that type.  For example, using the
-     * bearer id as the session id for {@link android.telephony.data.EpsBearerQosSessionAttributes}
-     * is a straight forward way to keep the sessions unique from one another within that type.
+     * Note: It can be either manufactured by the actor, but also may have a particular meaning
+     * within that type.  For example, using the bearer id as the session id for
+     * {@link android.telephony.data.EpsBearerQosSessionAttributes} is a straight forward way to
+     * keep the sessions unique from one another within that type.
      *
      * @return the id of the session
      */
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
index a45d507..39c2f33 100644
--- a/framework/src/android/net/QosSocketInfo.java
+++ b/framework/src/android/net/QosSocketInfo.java
@@ -16,6 +16,9 @@
 
 package android.net;
 
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -24,6 +27,7 @@
 import android.os.Parcelable;
 
 import java.io.IOException;
+import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
@@ -53,6 +57,8 @@
     @Nullable
     private final InetSocketAddress mRemoteSocketAddress;
 
+    private final int mSocketType;
+
     /**
      * The {@link Network} the socket is on.
      *
@@ -98,6 +104,16 @@
     }
 
     /**
+     * The socket type of the socket passed in when this QosSocketInfo object was constructed.
+     *
+     * @return the socket type of the socket.
+     * @hide
+     */
+    public int getSocketType()  {
+        return mSocketType;
+    }
+
+    /**
      * Creates a {@link QosSocketInfo} given a {@link Network} and bound {@link Socket}.  The
      * {@link Socket} must remain bound in order to receive {@link QosSession}s.
      *
@@ -112,6 +128,32 @@
         mParcelFileDescriptor = ParcelFileDescriptor.fromSocket(socket);
         mLocalSocketAddress =
                 new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort());
+        mSocketType = SOCK_STREAM;
+
+        if (socket.isConnected()) {
+            mRemoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
+        } else {
+            mRemoteSocketAddress = null;
+        }
+    }
+
+    /**
+     * Creates a {@link QosSocketInfo} given a {@link Network} and bound {@link DatagramSocket}. The
+     * {@link DatagramSocket} must remain bound in order to receive {@link QosSession}s.
+     *
+     * @param network the network
+     * @param socket the bound {@link DatagramSocket}
+     * @hide
+     */
+    public QosSocketInfo(@NonNull final Network network, @NonNull final DatagramSocket socket)
+            throws IOException {
+        Objects.requireNonNull(socket, "socket cannot be null");
+
+        mNetwork = Objects.requireNonNull(network, "network cannot be null");
+        mParcelFileDescriptor = ParcelFileDescriptor.fromDatagramSocket(socket);
+        mLocalSocketAddress =
+                new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort());
+        mSocketType = SOCK_DGRAM;
 
         if (socket.isConnected()) {
             mRemoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
@@ -131,6 +173,8 @@
         final int remoteAddressLength = in.readInt();
         mRemoteSocketAddress = remoteAddressLength == 0 ? null
                 : readSocketAddress(in, remoteAddressLength);
+
+        mSocketType = in.readInt();
     }
 
     private @NonNull InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) {
@@ -170,6 +214,7 @@
             dest.writeByteArray(remoteAddress);
             dest.writeInt(mRemoteSocketAddress.getPort());
         }
+        dest.writeInt(mSocketType);
     }
 
     @NonNull
diff --git a/framework/src/android/net/SocketLocalAddressChangedException.java b/framework/src/android/net/SocketLocalAddressChangedException.java
index 9daad83..7be3793 100644
--- a/framework/src/android/net/SocketLocalAddressChangedException.java
+++ b/framework/src/android/net/SocketLocalAddressChangedException.java
@@ -18,6 +18,8 @@
 
 import android.annotation.SystemApi;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * Thrown when the local address of the socket has changed.
  *
@@ -25,7 +27,7 @@
  */
 @SystemApi
 public class SocketLocalAddressChangedException extends Exception {
-    /** @hide */
+    @VisibleForTesting
     public SocketLocalAddressChangedException() {
         super("The local address of the socket changed");
     }
diff --git a/framework/src/android/net/SocketNotBoundException.java b/framework/src/android/net/SocketNotBoundException.java
index b1d7026..59f34a3 100644
--- a/framework/src/android/net/SocketNotBoundException.java
+++ b/framework/src/android/net/SocketNotBoundException.java
@@ -18,6 +18,8 @@
 
 import android.annotation.SystemApi;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * Thrown when a previously bound socket becomes unbound.
  *
@@ -25,7 +27,7 @@
  */
 @SystemApi
 public class SocketNotBoundException extends Exception {
-    /** @hide */
+    @VisibleForTesting
     public SocketNotBoundException() {
         super("The socket is unbound");
     }
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 9ddd2f5..280e497 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -49,6 +49,11 @@
 
     @NonNull private final ITestNetworkManager mService;
 
+    private static final boolean TAP = false;
+    private static final boolean TUN = true;
+    private static final boolean BRING_UP = true;
+    private static final LinkAddress[] NO_ADDRS = new LinkAddress[0];
+
     /** @hide */
     public TestNetworkManager(@NonNull ITestNetworkManager service) {
         mService = Objects.requireNonNull(service, "missing ITestNetworkManager");
@@ -155,7 +160,7 @@
     public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
         try {
             final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
-            return mService.createTunInterface(linkAddrs.toArray(arr));
+            return mService.createInterface(TUN, BRING_UP, linkAddrs.toArray(arr));
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -173,10 +178,28 @@
     @NonNull
     public TestNetworkInterface createTapInterface() {
         try {
-            return mService.createTapInterface();
+            return mService.createInterface(TAP, BRING_UP, NO_ADDRS);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
+    /**
+     * Create a tap interface for testing purposes
+     *
+     * @param bringUp whether to bring up the interface before returning it.
+     *
+     * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the
+     *     TAP interface.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTapInterface(boolean bringUp) {
+        try {
+            return mService.createInterface(TAP, bringUp, NO_ADDRS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/nearby/Android.bp b/nearby/Android.bp
index baa0740..fb4e3cd 100644
--- a/nearby/Android.bp
+++ b/nearby/Android.bp
@@ -30,8 +30,8 @@
 
 
 java_library {
-    name: "service-nearby",
-    srcs: [],
+    name: "service-nearby-pre-jarjar",
+    srcs: ["service-src/**/*.java"],
     sdk_version: "module_current",
     min_sdk_version: "30",
     apex_available: ["com.android.tethering"],
diff --git a/nearby/service-src/com/android/server/nearby/NearbyService.java b/nearby/service-src/com/android/server/nearby/NearbyService.java
new file mode 100644
index 0000000..88752cc
--- /dev/null
+++ b/nearby/service-src/com/android/server/nearby/NearbyService.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby;
+
+import android.content.Context;
+import android.os.Binder;
+
+/**
+ * Stub NearbyService class, used until NearbyService code is available in all branches.
+ *
+ * This can be published as an empty service in branches that use it.
+ */
+public final class NearbyService extends Binder {
+    public NearbyService(Context ctx) {
+        throw new UnsupportedOperationException("This is a stub service");
+    }
+
+    /** Called by the service initializer on each boot phase */
+    public void onBootPhase(int phase) {
+        // Do nothing
+    }
+}
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 3cd5e13..f3dfb57 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -134,6 +134,46 @@
         return -EPERM;
     }
 
+    // Note that tagging the socket to AID_CLAT is only implemented in JNI ClatCoordinator.
+    // The process is not allowed to tag socket to AID_CLAT via tagSocket() which would cause
+    // process data usage accounting to be bypassed. Tagging AID_CLAT is used for avoiding counting
+    // CLAT traffic data usage twice. See packages/modules/Connectivity/service/jni/
+    // com_android_server_connectivity_ClatCoordinator.cpp
+    if (chargeUid == AID_CLAT) {
+        return -EPERM;
+    }
+
+    // The socket destroy listener only monitors on the group {INET_TCP, INET_UDP, INET6_TCP,
+    // INET6_UDP}. Tagging listener unsupported socket causes that the tag can't be removed from
+    // tag map automatically. Eventually, the tag map may run out of space because of dead tag
+    // entries. Note that although tagSocket() of net client has already denied the family which
+    // is neither AF_INET nor AF_INET6, the family validation is still added here just in case.
+    // See tagSocket in system/netd/client/NetdClient.cpp and
+    // TrafficController::makeSkDestroyListener in
+    // packages/modules/Connectivity/service/native/TrafficController.cpp
+    // TODO: remove this once the socket destroy listener can detect more types of socket destroy.
+    int socketFamily;
+    socklen_t familyLen = sizeof(socketFamily);
+    if (getsockopt(sockFd, SOL_SOCKET, SO_DOMAIN, &socketFamily, &familyLen)) {
+        ALOGE("Failed to getsockopt SO_DOMAIN: %s, fd: %d", strerror(errno), sockFd);
+        return -errno;
+    }
+    if (socketFamily != AF_INET && socketFamily != AF_INET6) {
+        ALOGE("Unsupported family: %d", socketFamily);
+        return -EAFNOSUPPORT;
+    }
+
+    int socketProto;
+    socklen_t protoLen = sizeof(socketProto);
+    if (getsockopt(sockFd, SOL_SOCKET, SO_PROTOCOL, &socketProto, &protoLen)) {
+        ALOGE("Failed to getsockopt SO_PROTOCOL: %s, fd: %d", strerror(errno), sockFd);
+        return -errno;
+    }
+    if (socketProto != IPPROTO_UDP && socketProto != IPPROTO_TCP) {
+        ALOGE("Unsupported protocol: %d", socketProto);
+        return -EPROTONOSUPPORT;
+    }
+
     uint64_t sock_cookie = getSocketCookie(sockFd);
     if (sock_cookie == NONEXISTENT_COOKIE) return -errno;
     UidTagValue newKey = {.uid = (uint32_t)chargeUid, .tag = tag};
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index db59c7c..cd6b565 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -16,6 +16,7 @@
  * BpfHandlerTest.cpp - unit tests for BpfHandler.cpp
  */
 
+#include <private/android_filesystem_config.h>
 #include <sys/socket.h>
 
 #include <gtest/gtest.h>
@@ -187,6 +188,20 @@
     expectMapEmpty(mFakeCookieTagMap);
 }
 
+TEST_F(BpfHandlerTest, TestTagSocketWithUnsupportedFamily) {
+    int packetSocket = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    EXPECT_LE(0, packetSocket);
+    EXPECT_NE(NONEXISTENT_COOKIE, getSocketCookie(packetSocket));
+    EXPECT_EQ(-EAFNOSUPPORT, mBh.tagSocket(packetSocket, TEST_TAG, TEST_UID, TEST_UID));
+}
+
+TEST_F(BpfHandlerTest, TestTagSocketWithUnsupportedProtocol) {
+    int rawSocket = socket(AF_INET, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_RAW);
+    EXPECT_LE(0, rawSocket);
+    EXPECT_NE(NONEXISTENT_COOKIE, getSocketCookie(rawSocket));
+    EXPECT_EQ(-EPROTONOSUPPORT, mBh.tagSocket(rawSocket, TEST_TAG, TEST_UID, TEST_UID));
+}
+
 TEST_F(BpfHandlerTest, TestTagSocketWithoutPermission) {
     int sock = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
     ASSERT_NE(-1, sock);
@@ -208,6 +223,12 @@
     EXPECT_EQ(0, mBh.untagSocket(v6socket));
     expectNoTag(sockCookie);
     expectMapEmpty(mFakeCookieTagMap);
+
+    // Tag a socket to AID_CLAT other then realUid.
+    int sock = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
+    ASSERT_NE(-1, sock);
+    ASSERT_EQ(-EPERM, mBh.tagSocket(sock, TEST_TAG, AID_CLAT, realUid));
+    expectMapEmpty(mFakeCookieTagMap);
 }
 
 TEST_F(BpfHandlerTest, TestUntagInvalidSocket) {
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 8ba0768..1b9f2ec 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -19,27 +19,40 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-// This builds T+ services depending on framework-connectivity-tiramisu
+// Include build rules from Sources.bp
+build = ["Sources.bp"]
+
+filegroup {
+    name: "service-connectivity-tiramisu-sources",
+    srcs: [
+        "src/**/*.java",
+    ],
+    visibility: ["//visibility:private"],
+}
+// The above filegroup can be used to specify different sources depending
+// on the branch, while minimizing merge conflicts in the rest of the
+// build rules.
+
+// This builds T+ services depending on framework-connectivity-t
 // hidden symbols separately from the S+ services, to ensure that S+
 // services cannot accidentally depend on T+ hidden symbols from
-// framework-connectivity-tiramisu.
+// framework-connectivity-t.
 java_library {
     name: "service-connectivity-tiramisu-pre-jarjar",
     sdk_version: "system_server_current",
     // TODO(b/210962470): Bump this to at least S, and then T.
     min_sdk_version: "30",
     srcs: [
-        "src/**/*.java",
-        // TODO: This is necessary just for LocalLog, remove after removing NativeDaemonConnector.
-        ":framework-connectivity-shared-srcs",
-        ":services.connectivity-tiramisu-updatable-sources",
+        ":service-connectivity-tiramisu-sources",
     ],
     libs: [
         "framework-annotations-lib",
         "framework-connectivity-pre-jarjar",
-        "framework-connectivity-tiramisu-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
         "framework-tethering.stubs.module_lib",
         "service-connectivity-pre-jarjar",
+        "service-nearby-pre-jarjar",
+        "ServiceConnectivityResources",
         "unsupportedappusage",
     ],
     static_libs: [
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
new file mode 100644
index 0000000..187eadf
--- /dev/null
+++ b/service-t/Sources.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+// For test code only.
+filegroup {
+    name: "lib_networkStatsFactory_native",
+    srcs: [
+        "jni/com_android_server_net_NetworkStatsFactory.cpp",
+    ],
+    path: "jni",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+}
+
+filegroup {
+    name: "services.connectivity-netstats-jni-sources",
+    srcs: [
+        "jni/com_android_server_net_NetworkStatsFactory.cpp",
+        "jni/com_android_server_net_NetworkStatsService.cpp",
+    ],
+    path: "jni",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+}
+
diff --git a/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp b/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp
new file mode 100644
index 0000000..8b6526f
--- /dev/null
+++ b/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "NetworkStats"
+
+#include <errno.h>
+#include <inttypes.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <vector>
+
+#include <jni.h>
+
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
+
+#include <utils/Log.h>
+#include <utils/misc.h>
+
+#include "android-base/unique_fd.h"
+#include "bpf/BpfUtils.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+using android::bpf::parseBpfNetworkStatsDetail;
+using android::bpf::stats_line;
+
+namespace android {
+
+static jclass gStringClass;
+
+static struct {
+    jfieldID size;
+    jfieldID capacity;
+    jfieldID iface;
+    jfieldID uid;
+    jfieldID set;
+    jfieldID tag;
+    jfieldID metered;
+    jfieldID roaming;
+    jfieldID defaultNetwork;
+    jfieldID rxBytes;
+    jfieldID rxPackets;
+    jfieldID txBytes;
+    jfieldID txPackets;
+    jfieldID operations;
+} gNetworkStatsClassInfo;
+
+static jobjectArray get_string_array(JNIEnv* env, jobject obj, jfieldID field, int size, bool grow)
+{
+    if (!grow) {
+        jobjectArray array = (jobjectArray)env->GetObjectField(obj, field);
+        if (array != NULL) {
+            return array;
+        }
+    }
+    return env->NewObjectArray(size, gStringClass, NULL);
+}
+
+static jintArray get_int_array(JNIEnv* env, jobject obj, jfieldID field, int size, bool grow)
+{
+    if (!grow) {
+        jintArray array = (jintArray)env->GetObjectField(obj, field);
+        if (array != NULL) {
+            return array;
+        }
+    }
+    return env->NewIntArray(size);
+}
+
+static jlongArray get_long_array(JNIEnv* env, jobject obj, jfieldID field, int size, bool grow)
+{
+    if (!grow) {
+        jlongArray array = (jlongArray)env->GetObjectField(obj, field);
+        if (array != NULL) {
+            return array;
+        }
+    }
+    return env->NewLongArray(size);
+}
+
+static int legacyReadNetworkStatsDetail(std::vector<stats_line>* lines,
+                                        const std::vector<std::string>& limitIfaces,
+                                        int limitTag, int limitUid, const char* path) {
+    FILE* fp = fopen(path, "re");
+    if (fp == NULL) {
+        return -1;
+    }
+
+    int lastIdx = 1;
+    int idx;
+    char buffer[384];
+    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
+        stats_line s;
+        int64_t rawTag;
+        char* pos = buffer;
+        char* endPos;
+        // First field is the index.
+        idx = (int)strtol(pos, &endPos, 10);
+        //ALOGI("Index #%d: %s", idx, buffer);
+        if (pos == endPos) {
+            // Skip lines that don't start with in index.  In particular,
+            // this will skip the initial header line.
+            continue;
+        }
+        if (idx != lastIdx + 1) {
+            ALOGE("inconsistent idx=%d after lastIdx=%d: %s", idx, lastIdx, buffer);
+            fclose(fp);
+            return -1;
+        }
+        lastIdx = idx;
+        pos = endPos;
+        // Skip whitespace.
+        while (*pos == ' ') {
+            pos++;
+        }
+        // Next field is iface.
+        int ifaceIdx = 0;
+        while (*pos != ' ' && *pos != 0 && ifaceIdx < (int)(sizeof(s.iface)-1)) {
+            s.iface[ifaceIdx] = *pos;
+            ifaceIdx++;
+            pos++;
+        }
+        if (*pos != ' ') {
+            ALOGE("bad iface: %s", buffer);
+            fclose(fp);
+            return -1;
+        }
+        s.iface[ifaceIdx] = 0;
+        if (limitIfaces.size() > 0) {
+            // Is this an iface the caller is interested in?
+            int i = 0;
+            while (i < (int)limitIfaces.size()) {
+                if (limitIfaces[i] == s.iface) {
+                    break;
+                }
+                i++;
+            }
+            if (i >= (int)limitIfaces.size()) {
+                // Nothing matched; skip this line.
+                //ALOGI("skipping due to iface: %s", buffer);
+                continue;
+            }
+        }
+
+        // Ignore whitespace
+        while (*pos == ' ') pos++;
+
+        // Find end of tag field
+        endPos = pos;
+        while (*endPos != ' ') endPos++;
+
+        // Three digit field is always 0x0, otherwise parse
+        if (endPos - pos == 3) {
+            rawTag = 0;
+        } else {
+            if (sscanf(pos, "%" PRIx64, &rawTag) != 1) {
+                ALOGE("bad tag: %s", pos);
+                fclose(fp);
+                return -1;
+            }
+        }
+        s.tag = rawTag >> 32;
+        if (limitTag != -1 && s.tag != static_cast<uint32_t>(limitTag)) {
+            //ALOGI("skipping due to tag: %s", buffer);
+            continue;
+        }
+        pos = endPos;
+
+        // Ignore whitespace
+        while (*pos == ' ') pos++;
+
+        // Parse remaining fields.
+        if (sscanf(pos, "%u %u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64,
+                &s.uid, &s.set, &s.rxBytes, &s.rxPackets,
+                &s.txBytes, &s.txPackets) == 6) {
+            if (limitUid != -1 && static_cast<uint32_t>(limitUid) != s.uid) {
+                //ALOGI("skipping due to uid: %s", buffer);
+                continue;
+            }
+            lines->push_back(s);
+        } else {
+            //ALOGI("skipping due to bad remaining fields: %s", pos);
+        }
+    }
+
+    if (fclose(fp) != 0) {
+        ALOGE("Failed to close netstats file");
+        return -1;
+    }
+    return 0;
+}
+
+static int statsLinesToNetworkStats(JNIEnv* env, jclass clazz, jobject stats,
+                            std::vector<stats_line>& lines) {
+    int size = lines.size();
+
+    bool grow = size > env->GetIntField(stats, gNetworkStatsClassInfo.capacity);
+
+    ScopedLocalRef<jobjectArray> iface(env, get_string_array(env, stats,
+            gNetworkStatsClassInfo.iface, size, grow));
+    if (iface.get() == NULL) return -1;
+    ScopedIntArrayRW uid(env, get_int_array(env, stats,
+            gNetworkStatsClassInfo.uid, size, grow));
+    if (uid.get() == NULL) return -1;
+    ScopedIntArrayRW set(env, get_int_array(env, stats,
+            gNetworkStatsClassInfo.set, size, grow));
+    if (set.get() == NULL) return -1;
+    ScopedIntArrayRW tag(env, get_int_array(env, stats,
+            gNetworkStatsClassInfo.tag, size, grow));
+    if (tag.get() == NULL) return -1;
+    ScopedIntArrayRW metered(env, get_int_array(env, stats,
+            gNetworkStatsClassInfo.metered, size, grow));
+    if (metered.get() == NULL) return -1;
+    ScopedIntArrayRW roaming(env, get_int_array(env, stats,
+            gNetworkStatsClassInfo.roaming, size, grow));
+    if (roaming.get() == NULL) return -1;
+    ScopedIntArrayRW defaultNetwork(env, get_int_array(env, stats,
+            gNetworkStatsClassInfo.defaultNetwork, size, grow));
+    if (defaultNetwork.get() == NULL) return -1;
+    ScopedLongArrayRW rxBytes(env, get_long_array(env, stats,
+            gNetworkStatsClassInfo.rxBytes, size, grow));
+    if (rxBytes.get() == NULL) return -1;
+    ScopedLongArrayRW rxPackets(env, get_long_array(env, stats,
+            gNetworkStatsClassInfo.rxPackets, size, grow));
+    if (rxPackets.get() == NULL) return -1;
+    ScopedLongArrayRW txBytes(env, get_long_array(env, stats,
+            gNetworkStatsClassInfo.txBytes, size, grow));
+    if (txBytes.get() == NULL) return -1;
+    ScopedLongArrayRW txPackets(env, get_long_array(env, stats,
+            gNetworkStatsClassInfo.txPackets, size, grow));
+    if (txPackets.get() == NULL) return -1;
+    ScopedLongArrayRW operations(env, get_long_array(env, stats,
+            gNetworkStatsClassInfo.operations, size, grow));
+    if (operations.get() == NULL) return -1;
+
+    for (int i = 0; i < size; i++) {
+        ScopedLocalRef<jstring> ifaceString(env, env->NewStringUTF(lines[i].iface));
+        env->SetObjectArrayElement(iface.get(), i, ifaceString.get());
+
+        uid[i] = lines[i].uid;
+        set[i] = lines[i].set;
+        tag[i] = lines[i].tag;
+        // Metered, roaming and defaultNetwork are populated in Java-land.
+        rxBytes[i] = lines[i].rxBytes;
+        rxPackets[i] = lines[i].rxPackets;
+        txBytes[i] = lines[i].txBytes;
+        txPackets[i] = lines[i].txPackets;
+    }
+
+    env->SetIntField(stats, gNetworkStatsClassInfo.size, size);
+    if (grow) {
+        env->SetIntField(stats, gNetworkStatsClassInfo.capacity, size);
+        env->SetObjectField(stats, gNetworkStatsClassInfo.iface, iface.get());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.uid, uid.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.set, set.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.tag, tag.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.metered, metered.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.roaming, roaming.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.defaultNetwork,
+                defaultNetwork.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.rxBytes, rxBytes.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.rxPackets, rxPackets.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.txBytes, txBytes.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.txPackets, txPackets.getJavaArray());
+        env->SetObjectField(stats, gNetworkStatsClassInfo.operations, operations.getJavaArray());
+    }
+    return 0;
+}
+
+static int readNetworkStatsDetail(JNIEnv* env, jclass clazz, jobject stats, jstring path,
+                                  jint limitUid, jobjectArray limitIfacesObj, jint limitTag,
+                                  jboolean useBpfStats) {
+
+    std::vector<std::string> limitIfaces;
+    if (limitIfacesObj != NULL && env->GetArrayLength(limitIfacesObj) > 0) {
+        int num = env->GetArrayLength(limitIfacesObj);
+        for (int i = 0; i < num; i++) {
+            jstring string = (jstring)env->GetObjectArrayElement(limitIfacesObj, i);
+            ScopedUtfChars string8(env, string);
+            if (string8.c_str() != NULL) {
+                limitIfaces.push_back(std::string(string8.c_str()));
+            }
+        }
+    }
+    std::vector<stats_line> lines;
+
+
+    if (useBpfStats) {
+        if (parseBpfNetworkStatsDetail(&lines, limitIfaces, limitTag, limitUid) < 0)
+            return -1;
+    } else {
+        ScopedUtfChars path8(env, path);
+        if (path8.c_str() == NULL) {
+            ALOGE("the qtaguid legacy path is invalid: %s", path8.c_str());
+            return -1;
+        }
+        if (legacyReadNetworkStatsDetail(&lines, limitIfaces, limitTag,
+                                         limitUid, path8.c_str()) < 0)
+            return -1;
+    }
+
+    return statsLinesToNetworkStats(env, clazz, stats, lines);
+}
+
+static int readNetworkStatsDev(JNIEnv* env, jclass clazz, jobject stats) {
+    std::vector<stats_line> lines;
+
+    if (parseBpfNetworkStatsDev(&lines) < 0)
+            return -1;
+
+    return statsLinesToNetworkStats(env, clazz, stats, lines);
+}
+
+static const JNINativeMethod gMethods[] = {
+        { "nativeReadNetworkStatsDetail",
+                "(Landroid/net/NetworkStats;Ljava/lang/String;I[Ljava/lang/String;IZ)I",
+                (void*) readNetworkStatsDetail },
+        { "nativeReadNetworkStatsDev", "(Landroid/net/NetworkStats;)I",
+                (void*) readNetworkStatsDev },
+};
+
+int register_android_server_net_NetworkStatsFactory(JNIEnv* env) {
+    int err = jniRegisterNativeMethods(env, "com/android/server/net/NetworkStatsFactory", gMethods,
+            NELEM(gMethods));
+    gStringClass = env->FindClass("java/lang/String");
+    gStringClass = static_cast<jclass>(env->NewGlobalRef(gStringClass));
+
+    jclass clazz = env->FindClass("android/net/NetworkStats");
+    gNetworkStatsClassInfo.size = env->GetFieldID(clazz, "size", "I");
+    gNetworkStatsClassInfo.capacity = env->GetFieldID(clazz, "capacity", "I");
+    gNetworkStatsClassInfo.iface = env->GetFieldID(clazz, "iface", "[Ljava/lang/String;");
+    gNetworkStatsClassInfo.uid = env->GetFieldID(clazz, "uid", "[I");
+    gNetworkStatsClassInfo.set = env->GetFieldID(clazz, "set", "[I");
+    gNetworkStatsClassInfo.tag = env->GetFieldID(clazz, "tag", "[I");
+    gNetworkStatsClassInfo.metered = env->GetFieldID(clazz, "metered", "[I");
+    gNetworkStatsClassInfo.roaming = env->GetFieldID(clazz, "roaming", "[I");
+    gNetworkStatsClassInfo.defaultNetwork = env->GetFieldID(clazz, "defaultNetwork", "[I");
+    gNetworkStatsClassInfo.rxBytes = env->GetFieldID(clazz, "rxBytes", "[J");
+    gNetworkStatsClassInfo.rxPackets = env->GetFieldID(clazz, "rxPackets", "[J");
+    gNetworkStatsClassInfo.txBytes = env->GetFieldID(clazz, "txBytes", "[J");
+    gNetworkStatsClassInfo.txPackets = env->GetFieldID(clazz, "txPackets", "[J");
+    gNetworkStatsClassInfo.operations = env->GetFieldID(clazz, "operations", "[J");
+
+    return err;
+}
+
+}
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
new file mode 100644
index 0000000..39cbaf7
--- /dev/null
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "NetworkStatsNative"
+
+#include <cutils/qtaguid.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <jni.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <utils/Log.h>
+#include <utils/misc.h>
+
+#include "bpf/BpfUtils.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+using android::bpf::bpfGetUidStats;
+using android::bpf::bpfGetIfaceStats;
+
+namespace android {
+
+// NOTE: keep these in sync with TrafficStats.java
+static const uint64_t UNKNOWN = -1;
+
+enum StatsType {
+    RX_BYTES = 0,
+    RX_PACKETS = 1,
+    TX_BYTES = 2,
+    TX_PACKETS = 3,
+    TCP_RX_PACKETS = 4,
+    TCP_TX_PACKETS = 5
+};
+
+static uint64_t getStatsType(Stats* stats, StatsType type) {
+    switch (type) {
+        case RX_BYTES:
+            return stats->rxBytes;
+        case RX_PACKETS:
+            return stats->rxPackets;
+        case TX_BYTES:
+            return stats->txBytes;
+        case TX_PACKETS:
+            return stats->txPackets;
+        case TCP_RX_PACKETS:
+            return stats->tcpRxPackets;
+        case TCP_TX_PACKETS:
+            return stats->tcpTxPackets;
+        default:
+            return UNKNOWN;
+    }
+}
+
+static jlong getTotalStat(JNIEnv* env, jclass clazz, jint type) {
+    Stats stats = {};
+
+    if (bpfGetIfaceStats(NULL, &stats) == 0) {
+        return getStatsType(&stats, (StatsType) type);
+    } else {
+        return UNKNOWN;
+    }
+}
+
+static jlong getIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
+    ScopedUtfChars iface8(env, iface);
+    if (iface8.c_str() == NULL) {
+        return UNKNOWN;
+    }
+
+    Stats stats = {};
+
+    if (bpfGetIfaceStats(iface8.c_str(), &stats) == 0) {
+        return getStatsType(&stats, (StatsType) type);
+    } else {
+        return UNKNOWN;
+    }
+}
+
+static jlong getUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
+    Stats stats = {};
+
+    if (bpfGetUidStats(uid, &stats) == 0) {
+        return getStatsType(&stats, (StatsType) type);
+    } else {
+        return UNKNOWN;
+    }
+}
+
+static const JNINativeMethod gMethods[] = {
+        {"nativeGetTotalStat", "(I)J", (void*)getTotalStat},
+        {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)getIfaceStat},
+        {"nativeGetUidStat", "(II)J", (void*)getUidStat},
+};
+
+int register_android_server_net_NetworkStatsService(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/server/net/NetworkStatsService", gMethods,
+                                    NELEM(gMethods));
+}
+
+}
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index d24b14b..bf56fd5 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -41,7 +41,6 @@
         cfi: true,
     },
     apex_available: [
-        "//apex_available:platform",
         "com.android.tethering",
     ],
     min_sdk_version: "30",
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 67757af..626c2eb 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -20,28 +20,45 @@
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.ConstantsShim;
+import com.android.server.connectivity.ConnectivityNativeService;
+import com.android.server.ethernet.EthernetService;
+import com.android.server.ethernet.EthernetServiceImpl;
+import com.android.server.nearby.NearbyService;
 
 /**
  * Connectivity service initializer for core networking. This is called by system server to create
- * a new instance of ConnectivityService.
+ * a new instance of connectivity services.
  */
 public final class ConnectivityServiceInitializer extends SystemService {
     private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName();
+    private final ConnectivityNativeService mConnectivityNative;
     private final ConnectivityService mConnectivity;
     private final IpSecService mIpSecService;
     private final NsdService mNsdService;
+    private final NearbyService mNearbyService;
+    private final EthernetServiceImpl mEthernetServiceImpl;
 
     public ConnectivityServiceInitializer(Context context) {
         super(context);
         // Load JNI libraries used by ConnectivityService and its dependencies
         System.loadLibrary("service-connectivity");
+        mEthernetServiceImpl = createEthernetService(context);
         mConnectivity = new ConnectivityService(context);
         mIpSecService = createIpSecService(context);
+        mConnectivityNative = createConnectivityNativeService(context);
         mNsdService = createNsdService(context);
+        mNearbyService = createNearbyService(context);
     }
 
     @Override
     public void onStart() {
+        if (mEthernetServiceImpl != null) {
+            Log.i(TAG, "Registering " + Context.ETHERNET_SERVICE);
+            publishBinderService(Context.ETHERNET_SERVICE, mEthernetServiceImpl,
+                    /* allowIsolated= */ false);
+        }
+
         Log.i(TAG, "Registering " + Context.CONNECTIVITY_SERVICE);
         publishBinderService(Context.CONNECTIVITY_SERVICE, mConnectivity,
                 /* allowIsolated= */ false);
@@ -51,10 +68,34 @@
             publishBinderService(Context.IPSEC_SERVICE, mIpSecService, /* allowIsolated= */ false);
         }
 
+        if (mConnectivityNative != null) {
+            Log.i(TAG, "Registering " + ConnectivityNativeService.SERVICE_NAME);
+            publishBinderService(ConnectivityNativeService.SERVICE_NAME, mConnectivityNative,
+                    /* allowIsolated= */ false);
+        }
+
         if (mNsdService != null) {
             Log.i(TAG, "Registering " + Context.NSD_SERVICE);
             publishBinderService(Context.NSD_SERVICE, mNsdService, /* allowIsolated= */ false);
         }
+
+        if (mNearbyService != null) {
+            Log.i(TAG, "Registering " + ConstantsShim.NEARBY_SERVICE);
+            publishBinderService(ConstantsShim.NEARBY_SERVICE, mNearbyService,
+                    /* allowIsolated= */ false);
+        }
+
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (mNearbyService != null) {
+            mNearbyService.onBootPhase(phase);
+        }
+
+        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY && mEthernetServiceImpl != null) {
+            mEthernetServiceImpl.start();
+        }
     }
 
     /**
@@ -66,14 +107,47 @@
         return new IpSecService(context);
     }
 
+    /**
+     * Return ConnectivityNativeService instance, or null if current SDK is lower than T.
+     */
+    private ConnectivityNativeService createConnectivityNativeService(final Context context) {
+        if (!SdkLevel.isAtLeastT()) return null;
+        try {
+            return new ConnectivityNativeService(context);
+        } catch (UnsupportedOperationException e) {
+            Log.d(TAG, "Unable to get ConnectivityNative service", e);
+            return null;
+        }
+    }
+
     /** Return NsdService instance or null if current SDK is lower than T */
     private NsdService createNsdService(final Context context) {
         if (!SdkLevel.isAtLeastT()) return null;
+
+        return NsdService.create(context);
+    }
+
+    /** Return Nearby service instance or null if current SDK is lower than T */
+    private NearbyService createNearbyService(final Context context) {
+        if (!SdkLevel.isAtLeastT()) return null;
         try {
-            return NsdService.create(context);
-        } catch (InterruptedException e) {
-            Log.d(TAG, "Unable to get NSD service", e);
+            return new NearbyService(context);
+        } catch (UnsupportedOperationException e) {
+            // Nearby is not yet supported in all branches
+            // TODO: remove catch clause when it is available.
+            Log.i(TAG, "Skipping unsupported service " + ConstantsShim.NEARBY_SERVICE);
             return null;
         }
     }
+
+    /**
+     * Return EthernetServiceImpl instance or null if current SDK is lower than T or Ethernet
+     * service isn't necessary.
+     */
+    private EthernetServiceImpl createEthernetService(final Context context) {
+        if (!SdkLevel.isAtLeastT() || !mConnectivity.deviceSupportsEthernet(context)) {
+            return null;
+        }
+        return EthernetService.create(context);
+    }
 }
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
new file mode 100644
index 0000000..4bc40ea
--- /dev/null
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -0,0 +1,1878 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.Manifest.permission.DUMP;
+import static android.net.IpSecManager.INVALID_RESOURCE_ID;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.AF_UNSPEC;
+import static android.system.OsConstants.EINVAL;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.IIpSecService;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.IpSecAlgorithm;
+import android.net.IpSecConfig;
+import android.net.IpSecManager;
+import android.net.IpSecSpiResponse;
+import android.net.IpSecTransform;
+import android.net.IpSecTransformResponse;
+import android.net.IpSecTunnelInterfaceResponse;
+import android.net.IpSecUdpEncapResponse;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.TrafficStats;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Range;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.net.module.util.BinderUtils;
+import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.PermissionUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A service to manage multiple clients that want to access the IpSec API. The service is
+ * responsible for maintaining a list of clients and managing the resources (and related quotas)
+ * that each of them own.
+ *
+ * <p>Synchronization in IpSecService is done on all entrypoints due to potential race conditions at
+ * the kernel/xfrm level. Further, this allows the simplifying assumption to be made that only one
+ * thread is ever running at a time.
+ *
+ * @hide
+ */
+public class IpSecService extends IIpSecService.Stub {
+    private static final String TAG = "IpSecService";
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final int[] ADDRESS_FAMILIES =
+            new int[] {OsConstants.AF_INET, OsConstants.AF_INET6};
+
+    private static final int NETD_FETCH_TIMEOUT_MS = 5000; // ms
+    private static final InetAddress INADDR_ANY;
+
+    @VisibleForTesting static final int MAX_PORT_BIND_ATTEMPTS = 10;
+
+    private final INetd mNetd;
+
+    static {
+        try {
+            INADDR_ANY = InetAddress.getByAddress(new byte[] {0, 0, 0, 0});
+        } catch (UnknownHostException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static final int FREE_PORT_MIN = 1024; // ports 1-1023 are reserved
+    static final int PORT_MAX = 0xFFFF; // ports are an unsigned 16-bit integer
+
+    /* Binder context for this service */
+    private final Context mContext;
+    private final Dependencies mDeps;
+
+    /**
+     * The next non-repeating global ID for tracking resources between users, this service, and
+     * kernel data structures. Accessing this variable is not thread safe, so it is only read or
+     * modified within blocks synchronized on IpSecService.this. We want to avoid -1
+     * (INVALID_RESOURCE_ID) and 0 (we probably forgot to initialize it).
+     */
+    @GuardedBy("IpSecService.this")
+    private int mNextResourceId = 1;
+
+    /**
+     * Dependencies of IpSecService, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * Get a reference to INetd.
+         */
+        public INetd getNetdInstance(Context context) throws RemoteException {
+            final INetd netd = INetd.Stub.asInterface((IBinder)
+                    context.getSystemService(Context.NETD_SERVICE));
+            if (netd == null) {
+                throw new RemoteException("Failed to Get Netd Instance");
+            }
+            return netd;
+        }
+    }
+
+    final UidFdTagger mUidFdTagger;
+
+    /**
+     * Interface for user-reference and kernel-resource cleanup.
+     *
+     * <p>This interface must be implemented for a resource to be reference counted.
+     */
+    @VisibleForTesting
+    public interface IResource {
+        /**
+         * Invalidates a IResource object, ensuring it is invalid for the purposes of allocating new
+         * objects dependent on it.
+         *
+         * <p>Implementations of this method are expected to remove references to the IResource
+         * object from the IpSecService's tracking arrays. The removal from the arrays ensures that
+         * the resource is considered invalid for user access or allocation or use in other
+         * resources.
+         *
+         * <p>References to the IResource object may be held by other RefcountedResource objects,
+         * and as such, the underlying resources and quota may not be cleaned up.
+         */
+        void invalidate() throws RemoteException;
+
+        /**
+         * Releases underlying resources and related quotas.
+         *
+         * <p>Implementations of this method are expected to remove all system resources that are
+         * tracked by the IResource object. Due to other RefcountedResource objects potentially
+         * having references to the IResource object, freeUnderlyingResources may not always be
+         * called from releaseIfUnreferencedRecursively().
+         */
+        void freeUnderlyingResources() throws RemoteException;
+    }
+
+    /**
+     * RefcountedResource manages references and dependencies in an exclusively acyclic graph.
+     *
+     * <p>RefcountedResource implements both explicit and implicit resource management. Creating a
+     * RefcountedResource object creates an explicit reference that must be freed by calling
+     * userRelease(). Additionally, adding this object as a child of another RefcountedResource
+     * object will add an implicit reference.
+     *
+     * <p>Resources are cleaned up when all references, both implicit and explicit, are released
+     * (ie, when userRelease() is called and when all parents have called releaseReference() on this
+     * object.)
+     */
+    @VisibleForTesting
+    public class RefcountedResource<T extends IResource> implements IBinder.DeathRecipient {
+        private final T mResource;
+        private final List<RefcountedResource> mChildren;
+        int mRefCount = 1; // starts at 1 for user's reference.
+        IBinder mBinder;
+
+        RefcountedResource(T resource, IBinder binder, RefcountedResource... children) {
+            synchronized (IpSecService.this) {
+                this.mResource = resource;
+                this.mChildren = new ArrayList<>(children.length);
+                this.mBinder = binder;
+
+                for (RefcountedResource child : children) {
+                    mChildren.add(child);
+                    child.mRefCount++;
+                }
+
+                try {
+                    mBinder.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    binderDied();
+                    e.rethrowFromSystemServer();
+                }
+            }
+        }
+
+        /**
+         * If the Binder object dies, this function is called to free the system resources that are
+         * being tracked by this record and to subsequently release this record for garbage
+         * collection
+         */
+        @Override
+        public void binderDied() {
+            synchronized (IpSecService.this) {
+                try {
+                    userRelease();
+                } catch (Exception e) {
+                    Log.e(TAG, "Failed to release resource: " + e);
+                }
+            }
+        }
+
+        public T getResource() {
+            return mResource;
+        }
+
+        /**
+         * Unlinks from binder and performs IpSecService resource cleanup (removes from resource
+         * arrays)
+         *
+         * <p>If this method has been previously called, the RefcountedResource's binder field will
+         * be null, and the method will return without performing the cleanup a second time.
+         *
+         * <p>Note that calling this function does not imply that kernel resources will be freed at
+         * this time, or that the related quota will be returned. Such actions will only be
+         * performed upon the reference count reaching zero.
+         */
+        @GuardedBy("IpSecService.this")
+        public void userRelease() throws RemoteException {
+            // Prevent users from putting reference counts into a bad state by calling
+            // userRelease() multiple times.
+            if (mBinder == null) {
+                return;
+            }
+
+            mBinder.unlinkToDeath(this, 0);
+            mBinder = null;
+
+            mResource.invalidate();
+
+            releaseReference();
+        }
+
+        /**
+         * Removes a reference to this resource. If the resultant reference count is zero, the
+         * underlying resources are freed, and references to all child resources are also dropped
+         * recursively (resulting in them freeing their resources and children, etcetera)
+         *
+         * <p>This method also sets the reference count to an invalid value (-1) to signify that it
+         * has been fully released. Any subsequent calls to this method will result in an
+         * IllegalStateException being thrown due to resource already having been previously
+         * released
+         */
+        @VisibleForTesting
+        @GuardedBy("IpSecService.this")
+        public void releaseReference() throws RemoteException {
+            mRefCount--;
+
+            if (mRefCount > 0) {
+                return;
+            } else if (mRefCount < 0) {
+                throw new IllegalStateException(
+                        "Invalid operation - resource has already been released.");
+            }
+
+            // Cleanup own resources
+            mResource.freeUnderlyingResources();
+
+            // Cleanup child resources as needed
+            for (RefcountedResource<? extends IResource> child : mChildren) {
+                child.releaseReference();
+            }
+
+            // Enforce that resource cleanup can only be called once
+            // By decrementing the refcount (from 0 to -1), the next call will throw an
+            // IllegalStateException - it has already been released fully.
+            mRefCount--;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{mResource=")
+                    .append(mResource)
+                    .append(", mRefCount=")
+                    .append(mRefCount)
+                    .append(", mChildren=")
+                    .append(mChildren)
+                    .append("}")
+                    .toString();
+        }
+    }
+
+    /**
+     * Very simple counting class that looks much like a counting semaphore
+     *
+     * <p>This class is not thread-safe, and expects that that users of this class will ensure
+     * synchronization and thread safety by holding the IpSecService.this instance lock.
+     */
+    @VisibleForTesting
+    static class ResourceTracker {
+        private final int mMax;
+        int mCurrent;
+
+        ResourceTracker(int max) {
+            mMax = max;
+            mCurrent = 0;
+        }
+
+        boolean isAvailable() {
+            return (mCurrent < mMax);
+        }
+
+        void take() {
+            if (!isAvailable()) {
+                Log.wtf(TAG, "Too many resources allocated!");
+            }
+            mCurrent++;
+        }
+
+        void give() {
+            if (mCurrent <= 0) {
+                Log.wtf(TAG, "We've released this resource too many times");
+            }
+            mCurrent--;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{mCurrent=")
+                    .append(mCurrent)
+                    .append(", mMax=")
+                    .append(mMax)
+                    .append("}")
+                    .toString();
+        }
+    }
+
+    @VisibleForTesting
+    static final class UserRecord {
+        /* Maximum number of each type of resource that a single UID may possess */
+
+        // Up to 4 active VPNs/IWLAN with potential soft handover.
+        public static final int MAX_NUM_TUNNEL_INTERFACES = 8;
+        public static final int MAX_NUM_ENCAP_SOCKETS = 16;
+
+        // SPIs and Transforms are both cheap, and are 1:1 correlated.
+        public static final int MAX_NUM_TRANSFORMS = 64;
+        public static final int MAX_NUM_SPIS = 64;
+
+        /**
+         * Store each of the OwnedResource types in an (thinly wrapped) sparse array for indexing
+         * and explicit (user) reference management.
+         *
+         * <p>These are stored in separate arrays to improve debuggability and dump output clarity.
+         *
+         * <p>Resources are removed from this array when the user releases their explicit reference
+         * by calling one of the releaseResource() methods.
+         */
+        final RefcountedResourceArray<SpiRecord> mSpiRecords =
+                new RefcountedResourceArray<>(SpiRecord.class.getSimpleName());
+        final RefcountedResourceArray<TransformRecord> mTransformRecords =
+                new RefcountedResourceArray<>(TransformRecord.class.getSimpleName());
+        final RefcountedResourceArray<EncapSocketRecord> mEncapSocketRecords =
+                new RefcountedResourceArray<>(EncapSocketRecord.class.getSimpleName());
+        final RefcountedResourceArray<TunnelInterfaceRecord> mTunnelInterfaceRecords =
+                new RefcountedResourceArray<>(TunnelInterfaceRecord.class.getSimpleName());
+
+        /**
+         * Trackers for quotas for each of the OwnedResource types.
+         *
+         * <p>These trackers are separate from the resource arrays, since they are incremented and
+         * decremented at different points in time. Specifically, quota is only returned upon final
+         * resource deallocation (after all explicit and implicit references are released). Note
+         * that it is possible that calls to releaseResource() will not return the used quota if
+         * there are other resources that depend on (are parents of) the resource being released.
+         */
+        final ResourceTracker mSpiQuotaTracker = new ResourceTracker(MAX_NUM_SPIS);
+        final ResourceTracker mTransformQuotaTracker = new ResourceTracker(MAX_NUM_TRANSFORMS);
+        final ResourceTracker mSocketQuotaTracker = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS);
+        final ResourceTracker mTunnelQuotaTracker = new ResourceTracker(MAX_NUM_TUNNEL_INTERFACES);
+
+        void removeSpiRecord(int resourceId) {
+            mSpiRecords.remove(resourceId);
+        }
+
+        void removeTransformRecord(int resourceId) {
+            mTransformRecords.remove(resourceId);
+        }
+
+        void removeTunnelInterfaceRecord(int resourceId) {
+            mTunnelInterfaceRecords.remove(resourceId);
+        }
+
+        void removeEncapSocketRecord(int resourceId) {
+            mEncapSocketRecords.remove(resourceId);
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{mSpiQuotaTracker=")
+                    .append(mSpiQuotaTracker)
+                    .append(", mTransformQuotaTracker=")
+                    .append(mTransformQuotaTracker)
+                    .append(", mSocketQuotaTracker=")
+                    .append(mSocketQuotaTracker)
+                    .append(", mTunnelQuotaTracker=")
+                    .append(mTunnelQuotaTracker)
+                    .append(", mSpiRecords=")
+                    .append(mSpiRecords)
+                    .append(", mTransformRecords=")
+                    .append(mTransformRecords)
+                    .append(", mEncapSocketRecords=")
+                    .append(mEncapSocketRecords)
+                    .append(", mTunnelInterfaceRecords=")
+                    .append(mTunnelInterfaceRecords)
+                    .append("}")
+                    .toString();
+        }
+    }
+
+    /**
+     * This class is not thread-safe, and expects that that users of this class will ensure
+     * synchronization and thread safety by holding the IpSecService.this instance lock.
+     */
+    @VisibleForTesting
+    static final class UserResourceTracker {
+        private final SparseArray<UserRecord> mUserRecords = new SparseArray<>();
+
+        /** Lazy-initialization/getter that populates or retrieves the UserRecord as needed */
+        public UserRecord getUserRecord(int uid) {
+            checkCallerUid(uid);
+
+            UserRecord r = mUserRecords.get(uid);
+            if (r == null) {
+                r = new UserRecord();
+                mUserRecords.put(uid, r);
+            }
+            return r;
+        }
+
+        /** Safety method; guards against access of other user's UserRecords */
+        private void checkCallerUid(int uid) {
+            if (uid != Binder.getCallingUid() && Process.SYSTEM_UID != Binder.getCallingUid()) {
+                throw new SecurityException("Attempted access of unowned resources");
+            }
+        }
+
+        @Override
+        public String toString() {
+            return mUserRecords.toString();
+        }
+    }
+
+    @VisibleForTesting final UserResourceTracker mUserResourceTracker = new UserResourceTracker();
+
+    /**
+     * The OwnedResourceRecord class provides a facility to cleanly and reliably track system
+     * resources. It relies on a provided resourceId that should uniquely identify the kernel
+     * resource. To use this class, the user should implement the invalidate() and
+     * freeUnderlyingResources() methods that are responsible for cleaning up IpSecService resource
+     * tracking arrays and kernel resources, respectively.
+     *
+     * <p>This class associates kernel resources with the UID that owns and controls them.
+     */
+    private abstract class OwnedResourceRecord implements IResource {
+        final int mPid;
+        final int mUid;
+        protected final int mResourceId;
+
+        OwnedResourceRecord(int resourceId) {
+            super();
+            if (resourceId == INVALID_RESOURCE_ID) {
+                throw new IllegalArgumentException("Resource ID must not be INVALID_RESOURCE_ID");
+            }
+            mResourceId = resourceId;
+            mPid = Binder.getCallingPid();
+            mUid = Binder.getCallingUid();
+
+            getResourceTracker().take();
+        }
+
+        @Override
+        public abstract void invalidate() throws RemoteException;
+
+        /** Convenience method; retrieves the user resource record for the stored UID. */
+        protected UserRecord getUserRecord() {
+            return mUserResourceTracker.getUserRecord(mUid);
+        }
+
+        @Override
+        public abstract void freeUnderlyingResources() throws RemoteException;
+
+        /** Get the resource tracker for this resource */
+        protected abstract ResourceTracker getResourceTracker();
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{mResourceId=")
+                    .append(mResourceId)
+                    .append(", pid=")
+                    .append(mPid)
+                    .append(", uid=")
+                    .append(mUid)
+                    .append("}")
+                    .toString();
+        }
+    };
+
+    /**
+     * Thin wrapper over SparseArray to ensure resources exist, and simplify generic typing.
+     *
+     * <p>RefcountedResourceArray prevents null insertions, and throws an IllegalArgumentException
+     * if a key is not found during a retrieval process.
+     */
+    static class RefcountedResourceArray<T extends IResource> {
+        SparseArray<RefcountedResource<T>> mArray = new SparseArray<>();
+        private final String mTypeName;
+
+        RefcountedResourceArray(String typeName) {
+            this.mTypeName = typeName;
+        }
+
+        /**
+         * Accessor method to get inner resource object.
+         *
+         * @throws IllegalArgumentException if no resource with provided key is found.
+         */
+        T getResourceOrThrow(int key) {
+            return getRefcountedResourceOrThrow(key).getResource();
+        }
+
+        /**
+         * Accessor method to get reference counting wrapper.
+         *
+         * @throws IllegalArgumentException if no resource with provided key is found.
+         */
+        RefcountedResource<T> getRefcountedResourceOrThrow(int key) {
+            RefcountedResource<T> resource = mArray.get(key);
+            if (resource == null) {
+                throw new IllegalArgumentException(
+                        String.format("No such %s found for given id: %d", mTypeName, key));
+            }
+
+            return resource;
+        }
+
+        void put(int key, RefcountedResource<T> obj) {
+            Objects.requireNonNull(obj, "Null resources cannot be added");
+            mArray.put(key, obj);
+        }
+
+        void remove(int key) {
+            mArray.remove(key);
+        }
+
+        @Override
+        public String toString() {
+            return mArray.toString();
+        }
+    }
+
+    /**
+     * Tracks an SA in the kernel, and manages cleanup paths. Once a TransformRecord is
+     * created, the SpiRecord that originally tracked the SAs will reliquish the
+     * responsibility of freeing the underlying SA to this class via the mOwnedByTransform flag.
+     */
+    private final class TransformRecord extends OwnedResourceRecord {
+        private final IpSecConfig mConfig;
+        private final SpiRecord mSpi;
+        private final EncapSocketRecord mSocket;
+
+        TransformRecord(
+                int resourceId, IpSecConfig config, SpiRecord spi, EncapSocketRecord socket) {
+            super(resourceId);
+            mConfig = config;
+            mSpi = spi;
+            mSocket = socket;
+
+            spi.setOwnedByTransform();
+        }
+
+        public IpSecConfig getConfig() {
+            return mConfig;
+        }
+
+        public SpiRecord getSpiRecord() {
+            return mSpi;
+        }
+
+        public EncapSocketRecord getSocketRecord() {
+            return mSocket;
+        }
+
+        /** always guarded by IpSecService#this */
+        @Override
+        public void freeUnderlyingResources() {
+            int spi = mSpi.getSpi();
+            try {
+                mNetd.ipSecDeleteSecurityAssociation(
+                        mUid,
+                        mConfig.getSourceAddress(),
+                        mConfig.getDestinationAddress(),
+                        spi,
+                        mConfig.getMarkValue(),
+                        mConfig.getMarkMask(),
+                        mConfig.getXfrmInterfaceId());
+            } catch (RemoteException | ServiceSpecificException e) {
+                Log.e(TAG, "Failed to delete SA with ID: " + mResourceId, e);
+            }
+
+            getResourceTracker().give();
+        }
+
+        @Override
+        public void invalidate() throws RemoteException {
+            getUserRecord().removeTransformRecord(mResourceId);
+        }
+
+        @Override
+        protected ResourceTracker getResourceTracker() {
+            return getUserRecord().mTransformQuotaTracker;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder strBuilder = new StringBuilder();
+            strBuilder
+                    .append("{super=")
+                    .append(super.toString())
+                    .append(", mSocket=")
+                    .append(mSocket)
+                    .append(", mSpi.mResourceId=")
+                    .append(mSpi.mResourceId)
+                    .append(", mConfig=")
+                    .append(mConfig)
+                    .append("}");
+            return strBuilder.toString();
+        }
+    }
+
+    /**
+     * Tracks a single SA in the kernel, and manages cleanup paths. Once used in a Transform, the
+     * responsibility for cleaning up underlying resources will be passed to the TransformRecord
+     * object
+     */
+    private final class SpiRecord extends OwnedResourceRecord {
+        private final String mSourceAddress;
+        private final String mDestinationAddress;
+        private int mSpi;
+
+        private boolean mOwnedByTransform = false;
+
+        SpiRecord(int resourceId, String sourceAddress,
+                String destinationAddress, int spi) {
+            super(resourceId);
+            mSourceAddress = sourceAddress;
+            mDestinationAddress = destinationAddress;
+            mSpi = spi;
+        }
+
+        /** always guarded by IpSecService#this */
+        @Override
+        public void freeUnderlyingResources() {
+            try {
+                if (!mOwnedByTransform) {
+                    mNetd.ipSecDeleteSecurityAssociation(
+                            mUid, mSourceAddress, mDestinationAddress, mSpi, 0 /* mark */,
+                            0 /* mask */, 0 /* if_id */);
+                }
+            } catch (ServiceSpecificException | RemoteException e) {
+                Log.e(TAG, "Failed to delete SPI reservation with ID: " + mResourceId, e);
+            }
+
+            mSpi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
+
+            getResourceTracker().give();
+        }
+
+        public int getSpi() {
+            return mSpi;
+        }
+
+        public String getDestinationAddress() {
+            return mDestinationAddress;
+        }
+
+        public void setOwnedByTransform() {
+            if (mOwnedByTransform) {
+                // Programming error
+                throw new IllegalStateException("Cannot own an SPI twice!");
+            }
+
+            mOwnedByTransform = true;
+        }
+
+        public boolean getOwnedByTransform() {
+            return mOwnedByTransform;
+        }
+
+        @Override
+        public void invalidate() throws RemoteException {
+            getUserRecord().removeSpiRecord(mResourceId);
+        }
+
+        @Override
+        protected ResourceTracker getResourceTracker() {
+            return getUserRecord().mSpiQuotaTracker;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder strBuilder = new StringBuilder();
+            strBuilder
+                    .append("{super=")
+                    .append(super.toString())
+                    .append(", mSpi=")
+                    .append(mSpi)
+                    .append(", mSourceAddress=")
+                    .append(mSourceAddress)
+                    .append(", mDestinationAddress=")
+                    .append(mDestinationAddress)
+                    .append(", mOwnedByTransform=")
+                    .append(mOwnedByTransform)
+                    .append("}");
+            return strBuilder.toString();
+        }
+    }
+
+    private final SparseBooleanArray mTunnelNetIds = new SparseBooleanArray();
+    final Range<Integer> mNetIdRange = ConnectivityManager.getIpSecNetIdRange();
+    private int mNextTunnelNetId = mNetIdRange.getLower();
+
+    /**
+     * Reserves a netId within the range of netIds allocated for IPsec tunnel interfaces
+     *
+     * <p>This method should only be called from Binder threads. Do not call this from within the
+     * system server as it will crash the system on failure.
+     *
+     * @return an integer key within the netId range, if successful
+     * @throws IllegalStateException if unsuccessful (all netId are currently reserved)
+     */
+    @VisibleForTesting
+    int reserveNetId() {
+        final int range = mNetIdRange.getUpper() - mNetIdRange.getLower() + 1;
+        synchronized (mTunnelNetIds) {
+            for (int i = 0; i < range; i++) {
+                final int netId = mNextTunnelNetId;
+                if (++mNextTunnelNetId > mNetIdRange.getUpper()) {
+                    mNextTunnelNetId = mNetIdRange.getLower();
+                }
+                if (!mTunnelNetIds.get(netId)) {
+                    mTunnelNetIds.put(netId, true);
+                    return netId;
+                }
+            }
+        }
+        throw new IllegalStateException("No free netIds to allocate");
+    }
+
+    @VisibleForTesting
+    void releaseNetId(int netId) {
+        synchronized (mTunnelNetIds) {
+            mTunnelNetIds.delete(netId);
+        }
+    }
+
+    /**
+     * Tracks an tunnel interface, and manages cleanup paths.
+     *
+     * <p>This class is not thread-safe, and expects that that users of this class will ensure
+     * synchronization and thread safety by holding the IpSecService.this instance lock
+     */
+    @VisibleForTesting
+    final class TunnelInterfaceRecord extends OwnedResourceRecord {
+        private final String mInterfaceName;
+
+        // outer addresses
+        private final String mLocalAddress;
+        private final String mRemoteAddress;
+
+        private final int mIkey;
+        private final int mOkey;
+
+        private final int mIfId;
+
+        private Network mUnderlyingNetwork;
+
+        TunnelInterfaceRecord(
+                int resourceId,
+                String interfaceName,
+                Network underlyingNetwork,
+                String localAddr,
+                String remoteAddr,
+                int ikey,
+                int okey,
+                int intfId) {
+            super(resourceId);
+
+            mInterfaceName = interfaceName;
+            mUnderlyingNetwork = underlyingNetwork;
+            mLocalAddress = localAddr;
+            mRemoteAddress = remoteAddr;
+            mIkey = ikey;
+            mOkey = okey;
+            mIfId = intfId;
+        }
+
+        /** always guarded by IpSecService#this */
+        @Override
+        public void freeUnderlyingResources() {
+            // Calls to netd
+            //       Teardown VTI
+            //       Delete global policies
+            try {
+                mNetd.ipSecRemoveTunnelInterface(mInterfaceName);
+
+                for (int selAddrFamily : ADDRESS_FAMILIES) {
+                    mNetd.ipSecDeleteSecurityPolicy(
+                            mUid,
+                            selAddrFamily,
+                            IpSecManager.DIRECTION_OUT,
+                            mOkey,
+                            0xffffffff,
+                            mIfId);
+                    mNetd.ipSecDeleteSecurityPolicy(
+                            mUid,
+                            selAddrFamily,
+                            IpSecManager.DIRECTION_IN,
+                            mIkey,
+                            0xffffffff,
+                            mIfId);
+                }
+            } catch (ServiceSpecificException | RemoteException e) {
+                Log.e(
+                        TAG,
+                        "Failed to delete VTI with interface name: "
+                                + mInterfaceName
+                                + " and id: "
+                                + mResourceId, e);
+            }
+
+            getResourceTracker().give();
+            releaseNetId(mIkey);
+            releaseNetId(mOkey);
+        }
+
+        @GuardedBy("IpSecService.this")
+        public void setUnderlyingNetwork(Network underlyingNetwork) {
+            // When #applyTunnelModeTransform is called, this new underlying network will be used to
+            // update the output mark of the input transform.
+            mUnderlyingNetwork = underlyingNetwork;
+        }
+
+        @GuardedBy("IpSecService.this")
+        public Network getUnderlyingNetwork() {
+            return mUnderlyingNetwork;
+        }
+
+        public String getInterfaceName() {
+            return mInterfaceName;
+        }
+
+        /** Returns the local, outer address for the tunnelInterface */
+        public String getLocalAddress() {
+            return mLocalAddress;
+        }
+
+        /** Returns the remote, outer address for the tunnelInterface */
+        public String getRemoteAddress() {
+            return mRemoteAddress;
+        }
+
+        public int getIkey() {
+            return mIkey;
+        }
+
+        public int getOkey() {
+            return mOkey;
+        }
+
+        public int getIfId() {
+            return mIfId;
+        }
+
+        @Override
+        protected ResourceTracker getResourceTracker() {
+            return getUserRecord().mTunnelQuotaTracker;
+        }
+
+        @Override
+        public void invalidate() {
+            getUserRecord().removeTunnelInterfaceRecord(mResourceId);
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{super=")
+                    .append(super.toString())
+                    .append(", mInterfaceName=")
+                    .append(mInterfaceName)
+                    .append(", mUnderlyingNetwork=")
+                    .append(mUnderlyingNetwork)
+                    .append(", mLocalAddress=")
+                    .append(mLocalAddress)
+                    .append(", mRemoteAddress=")
+                    .append(mRemoteAddress)
+                    .append(", mIkey=")
+                    .append(mIkey)
+                    .append(", mOkey=")
+                    .append(mOkey)
+                    .append("}")
+                    .toString();
+        }
+    }
+
+    /**
+     * Tracks a UDP encap socket, and manages cleanup paths
+     *
+     * <p>While this class does not manage non-kernel resources, race conditions around socket
+     * binding require that the service creates the encap socket, binds it and applies the socket
+     * policy before handing it to a user.
+     */
+    private final class EncapSocketRecord extends OwnedResourceRecord {
+        private FileDescriptor mSocket;
+        private final int mPort;
+
+        EncapSocketRecord(int resourceId, FileDescriptor socket, int port) {
+            super(resourceId);
+            mSocket = socket;
+            mPort = port;
+        }
+
+        /** always guarded by IpSecService#this */
+        @Override
+        public void freeUnderlyingResources() {
+            Log.d(TAG, "Closing port " + mPort);
+            IoUtils.closeQuietly(mSocket);
+            mSocket = null;
+
+            getResourceTracker().give();
+        }
+
+        public int getPort() {
+            return mPort;
+        }
+
+        public FileDescriptor getFileDescriptor() {
+            return mSocket;
+        }
+
+        @Override
+        protected ResourceTracker getResourceTracker() {
+            return getUserRecord().mSocketQuotaTracker;
+        }
+
+        @Override
+        public void invalidate() {
+            getUserRecord().removeEncapSocketRecord(mResourceId);
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{super=")
+                    .append(super.toString())
+                    .append(", mSocket=")
+                    .append(mSocket)
+                    .append(", mPort=")
+                    .append(mPort)
+                    .append("}")
+                    .toString();
+        }
+    }
+
+    /**
+     * Constructs a new IpSecService instance
+     *
+     * @param context Binder context for this service
+     */
+    public IpSecService(Context context) {
+        this(context, new Dependencies());
+    }
+
+    @NonNull
+    private AppOpsManager getAppOpsManager() {
+        AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+        if (appOps == null) throw new RuntimeException("System Server couldn't get AppOps");
+        return appOps;
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public IpSecService(Context context, Dependencies deps) {
+        this(
+                context,
+                deps,
+                (fd, uid) -> {
+                    try {
+                        TrafficStats.setThreadStatsUid(uid);
+                        TrafficStats.tagFileDescriptor(fd);
+                    } finally {
+                        TrafficStats.clearThreadStatsUid();
+                    }
+                });
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public IpSecService(Context context, Dependencies deps, UidFdTagger uidFdTagger) {
+        mContext = context;
+        mDeps = Objects.requireNonNull(deps, "Missing dependencies.");
+        mUidFdTagger = uidFdTagger;
+        try {
+            mNetd = mDeps.getNetdInstance(mContext);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Checks that the provided InetAddress is valid for use in an IPsec SA. The address must not be
+     * a wildcard address and must be in a numeric form such as 1.2.3.4 or 2001::1.
+     */
+    private static void checkInetAddress(String inetAddress) {
+        if (TextUtils.isEmpty(inetAddress)) {
+            throw new IllegalArgumentException("Unspecified address");
+        }
+
+        InetAddress checkAddr = InetAddresses.parseNumericAddress(inetAddress);
+
+        if (checkAddr.isAnyLocalAddress()) {
+            throw new IllegalArgumentException("Inappropriate wildcard address: " + inetAddress);
+        }
+    }
+
+    /**
+     * Checks the user-provided direction field and throws an IllegalArgumentException if it is not
+     * DIRECTION_IN or DIRECTION_OUT
+     */
+    private void checkDirection(int direction) {
+        switch (direction) {
+            case IpSecManager.DIRECTION_OUT:
+            case IpSecManager.DIRECTION_IN:
+                return;
+            case IpSecManager.DIRECTION_FWD:
+                // Only NETWORK_STACK or MAINLINE_NETWORK_STACK allowed to use forward policies
+                PermissionUtils.enforceNetworkStackPermission(mContext);
+                return;
+        }
+        throw new IllegalArgumentException("Invalid Direction: " + direction);
+    }
+
+    /** Get a new SPI and maintain the reservation in the system server */
+    @Override
+    public synchronized IpSecSpiResponse allocateSecurityParameterIndex(
+            String destinationAddress, int requestedSpi, IBinder binder) throws RemoteException {
+        checkInetAddress(destinationAddress);
+        // RFC 4303 Section 2.1 - 0=local, 1-255=reserved.
+        if (requestedSpi > 0 && requestedSpi < 256) {
+            throw new IllegalArgumentException("ESP SPI must not be in the range of 0-255.");
+        }
+        Objects.requireNonNull(binder, "Null Binder passed to allocateSecurityParameterIndex");
+
+        int callingUid = Binder.getCallingUid();
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid);
+        final int resourceId = mNextResourceId++;
+
+        int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
+        try {
+            if (!userRecord.mSpiQuotaTracker.isAvailable()) {
+                return new IpSecSpiResponse(
+                        IpSecManager.Status.RESOURCE_UNAVAILABLE, INVALID_RESOURCE_ID, spi);
+            }
+
+            spi = mNetd.ipSecAllocateSpi(callingUid, "", destinationAddress, requestedSpi);
+            Log.d(TAG, "Allocated SPI " + spi);
+            userRecord.mSpiRecords.put(
+                    resourceId,
+                    new RefcountedResource<SpiRecord>(
+                            new SpiRecord(resourceId, "",
+                            destinationAddress, spi), binder));
+        } catch (ServiceSpecificException e) {
+            if (e.errorCode == OsConstants.ENOENT) {
+                return new IpSecSpiResponse(
+                        IpSecManager.Status.SPI_UNAVAILABLE, INVALID_RESOURCE_ID, spi);
+            }
+            throw e;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return new IpSecSpiResponse(IpSecManager.Status.OK, resourceId, spi);
+    }
+
+    /* This method should only be called from Binder threads. Do not call this from
+     * within the system server as it will crash the system on failure.
+     */
+    private void releaseResource(RefcountedResourceArray resArray, int resourceId)
+            throws RemoteException {
+        resArray.getRefcountedResourceOrThrow(resourceId).userRelease();
+    }
+
+    /** Release a previously allocated SPI that has been registered with the system server */
+    @Override
+    public synchronized void releaseSecurityParameterIndex(int resourceId) throws RemoteException {
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        releaseResource(userRecord.mSpiRecords, resourceId);
+    }
+
+    /**
+     * This function finds and forcibly binds to a random system port, ensuring that the port cannot
+     * be unbound.
+     *
+     * <p>A socket cannot be un-bound from a port if it was bound to that port by number. To select
+     * a random open port and then bind by number, this function creates a temp socket, binds to a
+     * random port (specifying 0), gets that port number, and then uses is to bind the user's UDP
+     * Encapsulation Socket forcibly, so that it cannot be un-bound by the user with the returned
+     * FileHandle.
+     *
+     * <p>The loop in this function handles the inherent race window between un-binding to a port
+     * and re-binding, during which the system could *technically* hand that port out to someone
+     * else.
+     */
+    private int bindToRandomPort(FileDescriptor sockFd) throws IOException {
+        for (int i = MAX_PORT_BIND_ATTEMPTS; i > 0; i--) {
+            try {
+                FileDescriptor probeSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+                Os.bind(probeSocket, INADDR_ANY, 0);
+                int port = ((InetSocketAddress) Os.getsockname(probeSocket)).getPort();
+                Os.close(probeSocket);
+                Log.v(TAG, "Binding to port " + port);
+                Os.bind(sockFd, INADDR_ANY, port);
+                return port;
+            } catch (ErrnoException e) {
+                // Someone miraculously claimed the port just after we closed probeSocket.
+                if (e.errno == OsConstants.EADDRINUSE) {
+                    continue;
+                }
+                throw e.rethrowAsIOException();
+            }
+        }
+        throw new IOException("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port");
+    }
+
+    /**
+     * Functional interface to do traffic tagging of given sockets to UIDs.
+     *
+     * <p>Specifically used by openUdpEncapsulationSocket to ensure data usage on the UDP encap
+     * sockets are billed to the UID that the UDP encap socket was created on behalf of.
+     *
+     * <p>Separate class so that the socket tagging logic can be mocked; TrafficStats uses static
+     * methods that cannot be easily mocked/tested.
+     */
+    @VisibleForTesting
+    public interface UidFdTagger {
+        /**
+         * Sets socket tag to assign all traffic to the provided UID.
+         *
+         * <p>Since the socket is created on behalf of an unprivileged application, all traffic
+         * should be accounted to the UID of the unprivileged application.
+         */
+        void tag(FileDescriptor fd, int uid) throws IOException;
+    }
+
+    /**
+     * Open a socket via the system server and bind it to the specified port (random if port=0).
+     * This will return a PFD to the user that represent a bound UDP socket. The system server will
+     * cache the socket and a record of its owner so that it can and must be freed when no longer
+     * needed.
+     */
+    @Override
+    public synchronized IpSecUdpEncapResponse openUdpEncapsulationSocket(int port, IBinder binder)
+            throws RemoteException {
+        if (port != 0 && (port < FREE_PORT_MIN || port > PORT_MAX)) {
+            throw new IllegalArgumentException(
+                    "Specified port number must be a valid non-reserved UDP port");
+        }
+        Objects.requireNonNull(binder, "Null Binder passed to openUdpEncapsulationSocket");
+
+        int callingUid = Binder.getCallingUid();
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid);
+        final int resourceId = mNextResourceId++;
+
+        ParcelFileDescriptor pFd = null;
+        try {
+            if (!userRecord.mSocketQuotaTracker.isAvailable()) {
+                return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
+            }
+
+            FileDescriptor sockFd = null;
+            try {
+                sockFd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+                pFd = ParcelFileDescriptor.dup(sockFd);
+            } finally {
+                IoUtils.closeQuietly(sockFd);
+            }
+
+            mUidFdTagger.tag(pFd.getFileDescriptor(), callingUid);
+            // This code is common to both the unspecified and specified port cases
+            Os.setsockoptInt(
+                    pFd.getFileDescriptor(),
+                    OsConstants.IPPROTO_UDP,
+                    OsConstants.UDP_ENCAP,
+                    OsConstants.UDP_ENCAP_ESPINUDP);
+
+            mNetd.ipSecSetEncapSocketOwner(pFd, callingUid);
+            if (port != 0) {
+                Log.v(TAG, "Binding to port " + port);
+                Os.bind(pFd.getFileDescriptor(), INADDR_ANY, port);
+            } else {
+                port = bindToRandomPort(pFd.getFileDescriptor());
+            }
+
+            userRecord.mEncapSocketRecords.put(
+                    resourceId,
+                    new RefcountedResource<EncapSocketRecord>(
+                            new EncapSocketRecord(resourceId, pFd.getFileDescriptor(), port),
+                            binder));
+            return new IpSecUdpEncapResponse(IpSecManager.Status.OK, resourceId, port,
+                    pFd.getFileDescriptor());
+        } catch (IOException | ErrnoException e) {
+            try {
+                if (pFd != null) {
+                    pFd.close();
+                }
+            } catch (IOException ex) {
+                // Nothing can be done at this point
+                Log.e(TAG, "Failed to close pFd.");
+            }
+        }
+        // If we make it to here, then something has gone wrong and we couldn't open a socket.
+        // The only reasonable condition that would cause that is resource unavailable.
+        return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
+    }
+
+    /** close a socket that has been been allocated by and registered with the system server */
+    @Override
+    public synchronized void closeUdpEncapsulationSocket(int resourceId) throws RemoteException {
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        releaseResource(userRecord.mEncapSocketRecords, resourceId);
+    }
+
+    /**
+     * Create a tunnel interface for use in IPSec tunnel mode. The system server will cache the
+     * tunnel interface and a record of its owner so that it can and must be freed when no longer
+     * needed.
+     */
+    @Override
+    public synchronized IpSecTunnelInterfaceResponse createTunnelInterface(
+            String localAddr, String remoteAddr, Network underlyingNetwork, IBinder binder,
+            String callingPackage) {
+        enforceTunnelFeatureAndPermissions(callingPackage);
+        Objects.requireNonNull(binder, "Null Binder passed to createTunnelInterface");
+        Objects.requireNonNull(underlyingNetwork, "No underlying network was specified");
+        checkInetAddress(localAddr);
+        checkInetAddress(remoteAddr);
+
+        // TODO: Check that underlying network exists, and IP addresses not assigned to a different
+        //       network (b/72316676).
+
+        int callerUid = Binder.getCallingUid();
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(callerUid);
+        if (!userRecord.mTunnelQuotaTracker.isAvailable()) {
+            return new IpSecTunnelInterfaceResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
+        }
+
+        final int resourceId = mNextResourceId++;
+        final int ikey = reserveNetId();
+        final int okey = reserveNetId();
+        String intfName = String.format("%s%d", INetd.IPSEC_INTERFACE_PREFIX, resourceId);
+
+        try {
+            // Calls to netd:
+            //       Create VTI
+            //       Add inbound/outbound global policies
+            //              (use reqid = 0)
+            mNetd.ipSecAddTunnelInterface(intfName, localAddr, remoteAddr, ikey, okey, resourceId);
+
+            BinderUtils.withCleanCallingIdentity(() -> {
+                NetdUtils.setInterfaceUp(mNetd, intfName);
+            });
+
+            for (int selAddrFamily : ADDRESS_FAMILIES) {
+                // Always send down correct local/remote addresses for template.
+                mNetd.ipSecAddSecurityPolicy(
+                        callerUid,
+                        selAddrFamily,
+                        IpSecManager.DIRECTION_OUT,
+                        localAddr,
+                        remoteAddr,
+                        0,
+                        okey,
+                        0xffffffff,
+                        resourceId);
+                mNetd.ipSecAddSecurityPolicy(
+                        callerUid,
+                        selAddrFamily,
+                        IpSecManager.DIRECTION_IN,
+                        remoteAddr,
+                        localAddr,
+                        0,
+                        ikey,
+                        0xffffffff,
+                        resourceId);
+
+                // Add a forwarding policy on the tunnel interface. In order to support forwarding
+                // the IpSecTunnelInterface must have a forwarding policy matching the incoming SA.
+                //
+                // Unless a IpSecTransform is also applied against this interface in DIRECTION_FWD,
+                // forwarding will be blocked by default (as would be the case if this policy was
+                // absent).
+                //
+                // This is necessary only on the tunnel interface, and not any the interface to
+                // which traffic will be forwarded to.
+                mNetd.ipSecAddSecurityPolicy(
+                        callerUid,
+                        selAddrFamily,
+                        IpSecManager.DIRECTION_FWD,
+                        remoteAddr,
+                        localAddr,
+                        0,
+                        ikey,
+                        0xffffffff,
+                        resourceId);
+            }
+
+            userRecord.mTunnelInterfaceRecords.put(
+                    resourceId,
+                    new RefcountedResource<TunnelInterfaceRecord>(
+                            new TunnelInterfaceRecord(
+                                    resourceId,
+                                    intfName,
+                                    underlyingNetwork,
+                                    localAddr,
+                                    remoteAddr,
+                                    ikey,
+                                    okey,
+                                    resourceId),
+                            binder));
+            return new IpSecTunnelInterfaceResponse(IpSecManager.Status.OK, resourceId, intfName);
+        } catch (RemoteException e) {
+            // Release keys if we got an error.
+            releaseNetId(ikey);
+            releaseNetId(okey);
+            throw e.rethrowFromSystemServer();
+        } catch (Throwable t) {
+            // Release keys if we got an error.
+            releaseNetId(ikey);
+            releaseNetId(okey);
+            throw t;
+        }
+    }
+
+    /**
+     * Adds a new local address to the tunnel interface. This allows packets to be sent and received
+     * from multiple local IP addresses over the same tunnel.
+     */
+    @Override
+    public synchronized void addAddressToTunnelInterface(
+            int tunnelResourceId, LinkAddress localAddr, String callingPackage) {
+        enforceTunnelFeatureAndPermissions(callingPackage);
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
+        // Get tunnelInterface record; if no such interface is found, will throw
+        // IllegalArgumentException
+        TunnelInterfaceRecord tunnelInterfaceInfo =
+                userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId);
+
+        try {
+            // We can assume general validity of the IP address, since we get them as a
+            // LinkAddress, which does some validation.
+            mNetd.interfaceAddAddress(
+                    tunnelInterfaceInfo.mInterfaceName,
+                    localAddr.getAddress().getHostAddress(),
+                    localAddr.getPrefixLength());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove a new local address from the tunnel interface. After removal, the address will no
+     * longer be available to send from, or receive on.
+     */
+    @Override
+    public synchronized void removeAddressFromTunnelInterface(
+            int tunnelResourceId, LinkAddress localAddr, String callingPackage) {
+        enforceTunnelFeatureAndPermissions(callingPackage);
+
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        // Get tunnelInterface record; if no such interface is found, will throw
+        // IllegalArgumentException
+        TunnelInterfaceRecord tunnelInterfaceInfo =
+                userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId);
+
+        try {
+            // We can assume general validity of the IP address, since we get them as a
+            // LinkAddress, which does some validation.
+            mNetd.interfaceDelAddress(
+                            tunnelInterfaceInfo.mInterfaceName,
+                            localAddr.getAddress().getHostAddress(),
+                            localAddr.getPrefixLength());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** Set TunnelInterface to use a specific underlying network. */
+    @Override
+    public synchronized void setNetworkForTunnelInterface(
+            int tunnelResourceId, Network underlyingNetwork, String callingPackage) {
+        enforceTunnelFeatureAndPermissions(callingPackage);
+        Objects.requireNonNull(underlyingNetwork, "No underlying network was specified");
+
+        final UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
+        // Get tunnelInterface record; if no such interface is found, will throw
+        // IllegalArgumentException. userRecord.mTunnelInterfaceRecords is never null
+        final TunnelInterfaceRecord tunnelInterfaceInfo =
+                userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId);
+
+        final ConnectivityManager connectivityManager =
+                mContext.getSystemService(ConnectivityManager.class);
+        final LinkProperties lp = connectivityManager.getLinkProperties(underlyingNetwork);
+        if (tunnelInterfaceInfo.getInterfaceName().equals(lp.getInterfaceName())) {
+            throw new IllegalArgumentException(
+                    "Underlying network cannot be the network being exposed by this tunnel");
+        }
+
+        // It is meaningless to check if the network exists or is valid because the network might
+        // disconnect at any time after it passes the check.
+
+        tunnelInterfaceInfo.setUnderlyingNetwork(underlyingNetwork);
+    }
+
+    /**
+     * Delete a TunnelInterface that has been been allocated by and registered with the system
+     * server
+     */
+    @Override
+    public synchronized void deleteTunnelInterface(
+            int resourceId, String callingPackage) throws RemoteException {
+        enforceTunnelFeatureAndPermissions(callingPackage);
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        releaseResource(userRecord.mTunnelInterfaceRecords, resourceId);
+    }
+
+    @VisibleForTesting
+    void validateAlgorithms(IpSecConfig config) throws IllegalArgumentException {
+        IpSecAlgorithm auth = config.getAuthentication();
+        IpSecAlgorithm crypt = config.getEncryption();
+        IpSecAlgorithm aead = config.getAuthenticatedEncryption();
+
+        // Validate the algorithm set
+        Preconditions.checkArgument(
+                aead != null || crypt != null || auth != null,
+                "No Encryption or Authentication algorithms specified");
+        Preconditions.checkArgument(
+                auth == null || auth.isAuthentication(),
+                "Unsupported algorithm for Authentication");
+        Preconditions.checkArgument(
+                crypt == null || crypt.isEncryption(), "Unsupported algorithm for Encryption");
+        Preconditions.checkArgument(
+                aead == null || aead.isAead(),
+                "Unsupported algorithm for Authenticated Encryption");
+        Preconditions.checkArgument(
+                aead == null || (auth == null && crypt == null),
+                "Authenticated Encryption is mutually exclusive with other Authentication "
+                        + "or Encryption algorithms");
+    }
+
+    private int getFamily(String inetAddress) {
+        int family = AF_UNSPEC;
+        InetAddress checkAddress = InetAddresses.parseNumericAddress(inetAddress);
+        if (checkAddress instanceof Inet4Address) {
+            family = AF_INET;
+        } else if (checkAddress instanceof Inet6Address) {
+            family = AF_INET6;
+        }
+        return family;
+    }
+
+    /**
+     * Checks an IpSecConfig parcel to ensure that the contents are valid and throws an
+     * IllegalArgumentException if they are not.
+     */
+    private void checkIpSecConfig(IpSecConfig config) {
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
+        switch (config.getEncapType()) {
+            case IpSecTransform.ENCAP_NONE:
+                break;
+            case IpSecTransform.ENCAP_ESPINUDP:
+            case IpSecTransform.ENCAP_ESPINUDP_NON_IKE:
+                // Retrieve encap socket record; will throw IllegalArgumentException if not found
+                userRecord.mEncapSocketRecords.getResourceOrThrow(
+                        config.getEncapSocketResourceId());
+
+                int port = config.getEncapRemotePort();
+                if (port <= 0 || port > 0xFFFF) {
+                    throw new IllegalArgumentException("Invalid remote UDP port: " + port);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid Encap Type: " + config.getEncapType());
+        }
+
+        validateAlgorithms(config);
+
+        // Retrieve SPI record; will throw IllegalArgumentException if not found
+        SpiRecord s = userRecord.mSpiRecords.getResourceOrThrow(config.getSpiResourceId());
+
+        // Check to ensure that SPI has not already been used.
+        if (s.getOwnedByTransform()) {
+            throw new IllegalStateException("SPI already in use; cannot be used in new Transforms");
+        }
+
+        // If no remote address is supplied, then use one from the SPI.
+        if (TextUtils.isEmpty(config.getDestinationAddress())) {
+            config.setDestinationAddress(s.getDestinationAddress());
+        }
+
+        // All remote addresses must match
+        if (!config.getDestinationAddress().equals(s.getDestinationAddress())) {
+            throw new IllegalArgumentException("Mismatched remote addresseses.");
+        }
+
+        // This check is technically redundant due to the chain of custody between the SPI and
+        // the IpSecConfig, but in the future if the dest is allowed to be set explicitly in
+        // the transform, this will prevent us from messing up.
+        checkInetAddress(config.getDestinationAddress());
+
+        // Require a valid source address for all transforms.
+        checkInetAddress(config.getSourceAddress());
+
+        // Check to ensure source and destination have the same address family.
+        String sourceAddress = config.getSourceAddress();
+        String destinationAddress = config.getDestinationAddress();
+        int sourceFamily = getFamily(sourceAddress);
+        int destinationFamily = getFamily(destinationAddress);
+        if (sourceFamily != destinationFamily) {
+            throw new IllegalArgumentException(
+                    "Source address ("
+                            + sourceAddress
+                            + ") and destination address ("
+                            + destinationAddress
+                            + ") have different address families.");
+        }
+
+        // Throw an error if UDP Encapsulation is not used in IPv4.
+        if (config.getEncapType() != IpSecTransform.ENCAP_NONE && sourceFamily != AF_INET) {
+            throw new IllegalArgumentException(
+                    "UDP Encapsulation is not supported for this address family");
+        }
+
+        switch (config.getMode()) {
+            case IpSecTransform.MODE_TRANSPORT:
+                break;
+            case IpSecTransform.MODE_TUNNEL:
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid IpSecTransform.mode: " + config.getMode());
+        }
+
+        config.setMarkValue(0);
+        config.setMarkMask(0);
+    }
+
+    private static final String TUNNEL_OP = AppOpsManager.OPSTR_MANAGE_IPSEC_TUNNELS;
+
+    private void enforceTunnelFeatureAndPermissions(String callingPackage) {
+        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) {
+            throw new UnsupportedOperationException(
+                    "IPsec Tunnel Mode requires PackageManager.FEATURE_IPSEC_TUNNELS");
+        }
+
+        Objects.requireNonNull(callingPackage, "Null calling package cannot create IpSec tunnels");
+
+        // OP_MANAGE_IPSEC_TUNNELS will return MODE_ERRORED by default, including for the system
+        // server. If the appop is not granted, require that the caller has the MANAGE_IPSEC_TUNNELS
+        // permission or is the System Server.
+        if (AppOpsManager.MODE_ALLOWED == getAppOpsManager().noteOpNoThrow(
+                TUNNEL_OP, Binder.getCallingUid(), callingPackage)) {
+            return;
+        }
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_IPSEC_TUNNELS, "IpSecService");
+    }
+
+    private void createOrUpdateTransform(
+            IpSecConfig c, int resourceId, SpiRecord spiRecord, EncapSocketRecord socketRecord)
+            throws RemoteException {
+
+        int encapType = c.getEncapType(), encapLocalPort = 0, encapRemotePort = 0;
+        if (encapType != IpSecTransform.ENCAP_NONE) {
+            encapLocalPort = socketRecord.getPort();
+            encapRemotePort = c.getEncapRemotePort();
+        }
+
+        IpSecAlgorithm auth = c.getAuthentication();
+        IpSecAlgorithm crypt = c.getEncryption();
+        IpSecAlgorithm authCrypt = c.getAuthenticatedEncryption();
+
+        String cryptName;
+        if (crypt == null) {
+            cryptName = (authCrypt == null) ? IpSecAlgorithm.CRYPT_NULL : "";
+        } else {
+            cryptName = crypt.getName();
+        }
+
+        mNetd.ipSecAddSecurityAssociation(
+                Binder.getCallingUid(),
+                c.getMode(),
+                c.getSourceAddress(),
+                c.getDestinationAddress(),
+                (c.getNetwork() != null) ? c.getNetwork().getNetId() : 0,
+                spiRecord.getSpi(),
+                c.getMarkValue(),
+                c.getMarkMask(),
+                (auth != null) ? auth.getName() : "",
+                (auth != null) ? auth.getKey() : new byte[] {},
+                (auth != null) ? auth.getTruncationLengthBits() : 0,
+                cryptName,
+                (crypt != null) ? crypt.getKey() : new byte[] {},
+                (crypt != null) ? crypt.getTruncationLengthBits() : 0,
+                (authCrypt != null) ? authCrypt.getName() : "",
+                (authCrypt != null) ? authCrypt.getKey() : new byte[] {},
+                (authCrypt != null) ? authCrypt.getTruncationLengthBits() : 0,
+                encapType,
+                encapLocalPort,
+                encapRemotePort,
+                c.getXfrmInterfaceId());
+    }
+
+    /**
+     * Create a IPsec transform, which represents a single security association in the kernel. The
+     * transform will be cached by the system server and must be freed when no longer needed. It is
+     * possible to free one, deleting the SA from underneath sockets that are using it, which will
+     * result in all of those sockets becoming unable to send or receive data.
+     */
+    @Override
+    public synchronized IpSecTransformResponse createTransform(
+            IpSecConfig c, IBinder binder, String callingPackage) throws RemoteException {
+        Objects.requireNonNull(c);
+        if (c.getMode() == IpSecTransform.MODE_TUNNEL) {
+            enforceTunnelFeatureAndPermissions(callingPackage);
+        }
+        checkIpSecConfig(c);
+        Objects.requireNonNull(binder, "Null Binder passed to createTransform");
+        final int resourceId = mNextResourceId++;
+
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        List<RefcountedResource> dependencies = new ArrayList<>();
+
+        if (!userRecord.mTransformQuotaTracker.isAvailable()) {
+            return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
+        }
+
+        EncapSocketRecord socketRecord = null;
+        if (c.getEncapType() != IpSecTransform.ENCAP_NONE) {
+            RefcountedResource<EncapSocketRecord> refcountedSocketRecord =
+                    userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(
+                            c.getEncapSocketResourceId());
+            dependencies.add(refcountedSocketRecord);
+            socketRecord = refcountedSocketRecord.getResource();
+        }
+
+        RefcountedResource<SpiRecord> refcountedSpiRecord =
+                userRecord.mSpiRecords.getRefcountedResourceOrThrow(c.getSpiResourceId());
+        dependencies.add(refcountedSpiRecord);
+        SpiRecord spiRecord = refcountedSpiRecord.getResource();
+
+        createOrUpdateTransform(c, resourceId, spiRecord, socketRecord);
+
+        // SA was created successfully, time to construct a record and lock it away
+        userRecord.mTransformRecords.put(
+                resourceId,
+                new RefcountedResource<TransformRecord>(
+                        new TransformRecord(resourceId, c, spiRecord, socketRecord),
+                        binder,
+                        dependencies.toArray(new RefcountedResource[dependencies.size()])));
+        return new IpSecTransformResponse(IpSecManager.Status.OK, resourceId);
+    }
+
+    /**
+     * Delete a transport mode transform that was previously allocated by + registered with the
+     * system server. If this is called on an inactive (or non-existent) transform, it will not
+     * return an error. It's safe to de-allocate transforms that may have already been deleted for
+     * other reasons.
+     */
+    @Override
+    public synchronized void deleteTransform(int resourceId) throws RemoteException {
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        releaseResource(userRecord.mTransformRecords, resourceId);
+    }
+
+    /**
+     * Apply an active transport mode transform to a socket, which will apply the IPsec security
+     * association as a correspondent policy to the provided socket
+     */
+    @Override
+    public synchronized void applyTransportModeTransform(
+            ParcelFileDescriptor socket, int direction, int resourceId) throws RemoteException {
+        int callingUid = Binder.getCallingUid();
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid);
+        checkDirection(direction);
+        // Get transform record; if no transform is found, will throw IllegalArgumentException
+        TransformRecord info = userRecord.mTransformRecords.getResourceOrThrow(resourceId);
+
+        // TODO: make this a function.
+        if (info.mPid != getCallingPid() || info.mUid != callingUid) {
+            throw new SecurityException("Only the owner of an IpSec Transform may apply it!");
+        }
+
+        // Get config and check that to-be-applied transform has the correct mode
+        IpSecConfig c = info.getConfig();
+        Preconditions.checkArgument(
+                c.getMode() == IpSecTransform.MODE_TRANSPORT,
+                "Transform mode was not Transport mode; cannot be applied to a socket");
+
+        mNetd.ipSecApplyTransportModeTransform(
+                socket,
+                callingUid,
+                direction,
+                c.getSourceAddress(),
+                c.getDestinationAddress(),
+                info.getSpiRecord().getSpi());
+    }
+
+    /**
+     * Remove transport mode transforms from a socket, applying the default (empty) policy. This
+     * ensures that NO IPsec policy is applied to the socket (would be the equivalent of applying a
+     * policy that performs no IPsec). Today the resourceId parameter is passed but not used:
+     * reserved for future improved input validation.
+     */
+    @Override
+    public synchronized void removeTransportModeTransforms(ParcelFileDescriptor socket)
+            throws RemoteException {
+        mNetd.ipSecRemoveTransportModeTransform(socket);
+    }
+
+    /**
+     * Apply an active tunnel mode transform to a TunnelInterface, which will apply the IPsec
+     * security association as a correspondent policy to the provided interface
+     */
+    @Override
+    public synchronized void applyTunnelModeTransform(
+            int tunnelResourceId, int direction,
+            int transformResourceId, String callingPackage) throws RemoteException {
+        enforceTunnelFeatureAndPermissions(callingPackage);
+        checkDirection(direction);
+
+        int callingUid = Binder.getCallingUid();
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid);
+
+        // Get transform record; if no transform is found, will throw IllegalArgumentException
+        TransformRecord transformInfo =
+                userRecord.mTransformRecords.getResourceOrThrow(transformResourceId);
+
+        // Get tunnelInterface record; if no such interface is found, will throw
+        // IllegalArgumentException
+        TunnelInterfaceRecord tunnelInterfaceInfo =
+                userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId);
+
+        // Get config and check that to-be-applied transform has the correct mode
+        IpSecConfig c = transformInfo.getConfig();
+        Preconditions.checkArgument(
+                c.getMode() == IpSecTransform.MODE_TUNNEL,
+                "Transform mode was not Tunnel mode; cannot be applied to a tunnel interface");
+
+        EncapSocketRecord socketRecord = null;
+        if (c.getEncapType() != IpSecTransform.ENCAP_NONE) {
+            socketRecord =
+                    userRecord.mEncapSocketRecords.getResourceOrThrow(c.getEncapSocketResourceId());
+        }
+        SpiRecord spiRecord = transformInfo.getSpiRecord();
+
+        int mark =
+                (direction == IpSecManager.DIRECTION_OUT)
+                        ? tunnelInterfaceInfo.getOkey()
+                        : tunnelInterfaceInfo.getIkey(); // Ikey also used for FWD policies
+
+        try {
+            // Default to using the invalid SPI of 0 for inbound SAs. This allows policies to skip
+            // SPI matching as part of the template resolution.
+            int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
+            c.setXfrmInterfaceId(tunnelInterfaceInfo.getIfId());
+
+            // TODO: enable this when UPDSA supports updating marks. Adding kernel support upstream
+            //     (and backporting) would allow us to narrow the mark space, and ensure that the SA
+            //     and SPs have matching marks (as VTI are meant to be built).
+            // Currently update does nothing with marks. Leave empty (defaulting to 0) to ensure the
+            //     config matches the actual allocated resources in the kernel.
+            // All SAs will have zero marks (from creation time), and any policy that matches the
+            //     same src/dst could match these SAs. Non-IpSecService governed processes that
+            //     establish floating policies with the same src/dst may result in undefined
+            //     behavior. This is generally limited to vendor code due to the permissions
+            //     (CAP_NET_ADMIN) required.
+            //
+            // c.setMarkValue(mark);
+            // c.setMarkMask(0xffffffff);
+
+            if (direction == IpSecManager.DIRECTION_OUT) {
+                // Set output mark via underlying network (output only)
+                c.setNetwork(tunnelInterfaceInfo.getUnderlyingNetwork());
+
+                // Set outbound SPI only. We want inbound to use any valid SA (old, new) on rekeys,
+                // but want to guarantee outbound packets are sent over the new SA.
+                spi = spiRecord.getSpi();
+            }
+
+            // Always update the policy with the relevant XFRM_IF_ID
+            for (int selAddrFamily : ADDRESS_FAMILIES) {
+                mNetd.ipSecUpdateSecurityPolicy(
+                        callingUid,
+                        selAddrFamily,
+                        direction,
+                        transformInfo.getConfig().getSourceAddress(),
+                        transformInfo.getConfig().getDestinationAddress(),
+                        spi, // If outbound, also add SPI to the policy.
+                        mark, // Must always set policy mark; ikey/okey for VTIs
+                        0xffffffff,
+                        c.getXfrmInterfaceId());
+            }
+
+            // Update SA with tunnel mark (ikey or okey based on direction)
+            createOrUpdateTransform(c, transformResourceId, spiRecord, socketRecord);
+        } catch (ServiceSpecificException e) {
+            if (e.errorCode == EINVAL) {
+                throw new IllegalArgumentException(e.toString());
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mContext.enforceCallingOrSelfPermission(DUMP, TAG);
+
+        pw.println("IpSecService dump:");
+        pw.println();
+
+        pw.println("mUserResourceTracker:");
+        pw.println(mUserResourceTracker);
+    }
+}
diff --git a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
new file mode 100644
index 0000000..0ea126a
--- /dev/null
+++ b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.content.Context;
+import android.net.TrafficStats;
+import android.util.Log;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.server.net.NetworkStatsService;
+
+/**
+ * NetworkStats service initializer for core networking. This is called by system server to create
+ * a new instance of NetworkStatsService.
+ */
+public final class NetworkStatsServiceInitializer extends SystemService {
+    private static final String TAG = NetworkStatsServiceInitializer.class.getSimpleName();
+    private final NetworkStatsService mStatsService;
+
+    public NetworkStatsServiceInitializer(Context context) {
+        super(context);
+        // Load JNI libraries used by NetworkStatsService and its dependencies
+        System.loadLibrary("service-connectivity");
+        mStatsService = maybeCreateNetworkStatsService(context);
+    }
+
+    @Override
+    public void onStart() {
+        if (mStatsService != null) {
+            Log.i(TAG, "Registering " + Context.NETWORK_STATS_SERVICE);
+            publishBinderService(Context.NETWORK_STATS_SERVICE, mStatsService,
+                    /* allowIsolated= */ false);
+            TrafficStats.init(getContext());
+        }
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        // This has to be run before StatsPullAtomService query usage at
+        // PHASE_THIRD_PARTY_APPS_CAN_START.
+        if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY && mStatsService != null) {
+            mStatsService.systemReady();
+        }
+    }
+
+    /**
+     * Return NetworkStatsService instance, or null if current SDK is lower than T.
+     */
+    private NetworkStatsService maybeCreateNetworkStatsService(final Context context) {
+        if (!SdkLevel.isAtLeastT()) return null;
+
+        return NetworkStatsService.create(context);
+    }
+}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
new file mode 100644
index 0000000..4086e4e
--- /dev/null
+++ b/service-t/src/com/android/server/NsdService.java
@@ -0,0 +1,1022 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.net.ConnectivityManager.NETID_UNSET;
+import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.RegistrationInfo;
+import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.INsdManager;
+import android.net.nsd.INsdManagerCallback;
+import android.net.nsd.INsdServiceConnector;
+import android.net.nsd.MDnsManager;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+
+/**
+ * Network Service Discovery Service handles remote service discovery operation requests by
+ * implementing the INsdManager interface.
+ *
+ * @hide
+ */
+public class NsdService extends INsdManager.Stub {
+    private static final String TAG = "NsdService";
+    private static final String MDNS_TAG = "mDnsConnector";
+
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final long CLEANUP_DELAY_MS = 10000;
+    private static final int IFACE_IDX_ANY = 0;
+
+    private final Context mContext;
+    private final NsdStateMachine mNsdStateMachine;
+    private final MDnsManager mMDnsManager;
+    private final MDnsEventCallback mMDnsEventCallback;
+    // WARNING : Accessing this value in any thread is not safe, it must only be changed in the
+    // state machine thread. If change this outside state machine, it will need to introduce
+    // synchronization.
+    private boolean mIsDaemonStarted = false;
+
+    /**
+     * Clients receiving asynchronous messages
+     */
+    private final HashMap<NsdServiceConnector, ClientInfo> mClients = new HashMap<>();
+
+    /* A map from unique id to client info */
+    private final SparseArray<ClientInfo> mIdToClientInfoMap= new SparseArray<>();
+
+    private final long mCleanupDelayMs;
+
+    private static final int INVALID_ID = 0;
+    private int mUniqueId = 1;
+    // The count of the connected legacy clients.
+    private int mLegacyClientCount = 0;
+
+    private class NsdStateMachine extends StateMachine {
+
+        private final DefaultState mDefaultState = new DefaultState();
+        private final DisabledState mDisabledState = new DisabledState();
+        private final EnabledState mEnabledState = new EnabledState();
+
+        @Override
+        protected String getWhatToString(int what) {
+            return NsdManager.nameOf(what);
+        }
+
+        private void maybeStartDaemon() {
+            if (mIsDaemonStarted) {
+                if (DBG) Log.d(TAG, "Daemon is already started.");
+                return;
+            }
+            mMDnsManager.registerEventListener(mMDnsEventCallback);
+            mMDnsManager.startDaemon();
+            mIsDaemonStarted = true;
+            maybeScheduleStop();
+        }
+
+        private void maybeStopDaemon() {
+            if (!mIsDaemonStarted) {
+                if (DBG) Log.d(TAG, "Daemon has not been started.");
+                return;
+            }
+            mMDnsManager.unregisterEventListener(mMDnsEventCallback);
+            mMDnsManager.stopDaemon();
+            mIsDaemonStarted = false;
+        }
+
+        private boolean isAnyRequestActive() {
+            return mIdToClientInfoMap.size() != 0;
+        }
+
+        private void scheduleStop() {
+            sendMessageDelayed(NsdManager.DAEMON_CLEANUP, mCleanupDelayMs);
+        }
+        private void maybeScheduleStop() {
+            // The native daemon should stay alive and can't be cleanup
+            // if any legacy client connected.
+            if (!isAnyRequestActive() && mLegacyClientCount == 0) {
+                scheduleStop();
+            }
+        }
+
+        private void cancelStop() {
+            this.removeMessages(NsdManager.DAEMON_CLEANUP);
+        }
+
+        NsdStateMachine(String name, Handler handler) {
+            super(name, handler);
+            addState(mDefaultState);
+                addState(mDisabledState, mDefaultState);
+                addState(mEnabledState, mDefaultState);
+            State initialState = mEnabledState;
+            setInitialState(initialState);
+            setLogRecSize(25);
+        }
+
+        class DefaultState extends State {
+            @Override
+            public boolean processMessage(Message msg) {
+                final ClientInfo cInfo;
+                final int clientId = msg.arg2;
+                switch (msg.what) {
+                    case NsdManager.REGISTER_CLIENT:
+                        final Pair<NsdServiceConnector, INsdManagerCallback> arg =
+                                (Pair<NsdServiceConnector, INsdManagerCallback>) msg.obj;
+                        final INsdManagerCallback cb = arg.second;
+                        try {
+                            cb.asBinder().linkToDeath(arg.first, 0);
+                            cInfo = new ClientInfo(cb);
+                            mClients.put(arg.first, cInfo);
+                        } catch (RemoteException e) {
+                            Log.w(TAG, "Client " + clientId + " has already died");
+                        }
+                        break;
+                    case NsdManager.UNREGISTER_CLIENT:
+                        final NsdServiceConnector connector = (NsdServiceConnector) msg.obj;
+                        cInfo = mClients.remove(connector);
+                        if (cInfo != null) {
+                            cInfo.expungeAllRequests();
+                            if (cInfo.isLegacy()) {
+                                mLegacyClientCount -= 1;
+                            }
+                        }
+                        maybeScheduleStop();
+                        break;
+                    case NsdManager.DISCOVER_SERVICES:
+                        cInfo = getClientInfoForReply(msg);
+                        if (cInfo != null) {
+                            cInfo.onDiscoverServicesFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                       break;
+                    case NsdManager.STOP_DISCOVERY:
+                        cInfo = getClientInfoForReply(msg);
+                        if (cInfo != null) {
+                            cInfo.onStopDiscoveryFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case NsdManager.REGISTER_SERVICE:
+                        cInfo = getClientInfoForReply(msg);
+                        if (cInfo != null) {
+                            cInfo.onRegisterServiceFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case NsdManager.UNREGISTER_SERVICE:
+                        cInfo = getClientInfoForReply(msg);
+                        if (cInfo != null) {
+                            cInfo.onUnregisterServiceFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case NsdManager.RESOLVE_SERVICE:
+                        cInfo = getClientInfoForReply(msg);
+                        if (cInfo != null) {
+                            cInfo.onResolveServiceFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case NsdManager.DAEMON_CLEANUP:
+                        maybeStopDaemon();
+                        break;
+                    // This event should be only sent by the legacy (target SDK < S) clients.
+                    // Mark the sending client as legacy.
+                    case NsdManager.DAEMON_STARTUP:
+                        cInfo = getClientInfoForReply(msg);
+                        if (cInfo != null) {
+                            cancelStop();
+                            cInfo.setLegacy();
+                            mLegacyClientCount += 1;
+                            maybeStartDaemon();
+                        }
+                        break;
+                    default:
+                        Log.e(TAG, "Unhandled " + msg);
+                        return NOT_HANDLED;
+                }
+                return HANDLED;
+            }
+
+            private ClientInfo getClientInfoForReply(Message msg) {
+                final ListenerArgs args = (ListenerArgs) msg.obj;
+                return mClients.get(args.connector);
+            }
+        }
+
+        class DisabledState extends State {
+            @Override
+            public void enter() {
+                sendNsdStateChangeBroadcast(false);
+            }
+
+            @Override
+            public boolean processMessage(Message msg) {
+                switch (msg.what) {
+                    case NsdManager.ENABLE:
+                        transitionTo(mEnabledState);
+                        break;
+                    default:
+                        return NOT_HANDLED;
+                }
+                return HANDLED;
+            }
+        }
+
+        class EnabledState extends State {
+            @Override
+            public void enter() {
+                sendNsdStateChangeBroadcast(true);
+            }
+
+            @Override
+            public void exit() {
+                // TODO: it is incorrect to stop the daemon without expunging all requests
+                // and sending error callbacks to clients.
+                scheduleStop();
+            }
+
+            private boolean requestLimitReached(ClientInfo clientInfo) {
+                if (clientInfo.mClientIds.size() >= ClientInfo.MAX_LIMIT) {
+                    if (DBG) Log.d(TAG, "Exceeded max outstanding requests " + clientInfo);
+                    return true;
+                }
+                return false;
+            }
+
+            private void storeRequestMap(int clientId, int globalId, ClientInfo clientInfo, int what) {
+                clientInfo.mClientIds.put(clientId, globalId);
+                clientInfo.mClientRequests.put(clientId, what);
+                mIdToClientInfoMap.put(globalId, clientInfo);
+                // Remove the cleanup event because here comes a new request.
+                cancelStop();
+            }
+
+            private void removeRequestMap(int clientId, int globalId, ClientInfo clientInfo) {
+                clientInfo.mClientIds.delete(clientId);
+                clientInfo.mClientRequests.delete(clientId);
+                mIdToClientInfoMap.remove(globalId);
+                maybeScheduleStop();
+            }
+
+            @Override
+            public boolean processMessage(Message msg) {
+                final ClientInfo clientInfo;
+                final int id;
+                final int clientId = msg.arg2;
+                final ListenerArgs args;
+                switch (msg.what) {
+                    case NsdManager.DISABLE:
+                        //TODO: cleanup clients
+                        transitionTo(mDisabledState);
+                        break;
+                    case NsdManager.DISCOVER_SERVICES:
+                        if (DBG) Log.d(TAG, "Discover services");
+                        args = (ListenerArgs) msg.obj;
+                        clientInfo = mClients.get(args.connector);
+
+                        if (requestLimitReached(clientInfo)) {
+                            clientInfo.onDiscoverServicesFailed(
+                                    clientId, NsdManager.FAILURE_MAX_LIMIT);
+                            break;
+                        }
+
+                        maybeStartDaemon();
+                        id = getUniqueId();
+                        if (discoverServices(id, args.serviceInfo)) {
+                            if (DBG) {
+                                Log.d(TAG, "Discover " + msg.arg2 + " " + id
+                                        + args.serviceInfo.getServiceType());
+                            }
+                            storeRequestMap(clientId, id, clientInfo, msg.what);
+                            clientInfo.onDiscoverServicesStarted(clientId, args.serviceInfo);
+                        } else {
+                            stopServiceDiscovery(id);
+                            clientInfo.onDiscoverServicesFailed(clientId,
+                                    NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case NsdManager.STOP_DISCOVERY:
+                        if (DBG) Log.d(TAG, "Stop service discovery");
+                        args = (ListenerArgs) msg.obj;
+                        clientInfo = mClients.get(args.connector);
+
+                        try {
+                            id = clientInfo.mClientIds.get(clientId);
+                        } catch (NullPointerException e) {
+                            clientInfo.onStopDiscoveryFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            break;
+                        }
+                        removeRequestMap(clientId, id, clientInfo);
+                        if (stopServiceDiscovery(id)) {
+                            clientInfo.onStopDiscoverySucceeded(clientId);
+                        } else {
+                            clientInfo.onStopDiscoveryFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case NsdManager.REGISTER_SERVICE:
+                        if (DBG) Log.d(TAG, "Register service");
+                        args = (ListenerArgs) msg.obj;
+                        clientInfo = mClients.get(args.connector);
+                        if (requestLimitReached(clientInfo)) {
+                            clientInfo.onRegisterServiceFailed(
+                                    clientId, NsdManager.FAILURE_MAX_LIMIT);
+                            break;
+                        }
+
+                        maybeStartDaemon();
+                        id = getUniqueId();
+                        if (registerService(id, args.serviceInfo)) {
+                            if (DBG) Log.d(TAG, "Register " + clientId + " " + id);
+                            storeRequestMap(clientId, id, clientInfo, msg.what);
+                            // Return success after mDns reports success
+                        } else {
+                            unregisterService(id);
+                            clientInfo.onRegisterServiceFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case NsdManager.UNREGISTER_SERVICE:
+                        if (DBG) Log.d(TAG, "unregister service");
+                        args = (ListenerArgs) msg.obj;
+                        clientInfo = mClients.get(args.connector);
+                        if (clientInfo == null) {
+                            Log.e(TAG, "Unknown connector in unregistration");
+                            break;
+                        }
+                        id = clientInfo.mClientIds.get(clientId);
+                        removeRequestMap(clientId, id, clientInfo);
+                        if (unregisterService(id)) {
+                            clientInfo.onUnregisterServiceSucceeded(clientId);
+                        } else {
+                            clientInfo.onUnregisterServiceFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case NsdManager.RESOLVE_SERVICE:
+                        if (DBG) Log.d(TAG, "Resolve service");
+                        args = (ListenerArgs) msg.obj;
+                        clientInfo = mClients.get(args.connector);
+
+                        if (clientInfo.mResolvedService != null) {
+                            clientInfo.onResolveServiceFailed(
+                                    clientId, NsdManager.FAILURE_ALREADY_ACTIVE);
+                            break;
+                        }
+
+                        maybeStartDaemon();
+                        id = getUniqueId();
+                        if (resolveService(id, args.serviceInfo)) {
+                            clientInfo.mResolvedService = new NsdServiceInfo();
+                            storeRequestMap(clientId, id, clientInfo, msg.what);
+                        } else {
+                            clientInfo.onResolveServiceFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        break;
+                    case MDNS_SERVICE_EVENT:
+                        if (!handleMDnsServiceEvent(msg.arg1, msg.arg2, msg.obj)) {
+                            return NOT_HANDLED;
+                        }
+                        break;
+                    default:
+                        return NOT_HANDLED;
+                }
+                return HANDLED;
+            }
+
+            private boolean handleMDnsServiceEvent(int code, int id, Object obj) {
+                NsdServiceInfo servInfo;
+                ClientInfo clientInfo = mIdToClientInfoMap.get(id);
+                if (clientInfo == null) {
+                    Log.e(TAG, String.format("id %d for %d has no client mapping", id, code));
+                    return false;
+                }
+
+                /* This goes in response as msg.arg2 */
+                int clientId = clientInfo.getClientId(id);
+                if (clientId < 0) {
+                    // This can happen because of race conditions. For example,
+                    // SERVICE_FOUND may race with STOP_SERVICE_DISCOVERY,
+                    // and we may get in this situation.
+                    Log.d(TAG, String.format("%d for listener id %d that is no longer active",
+                            code, id));
+                    return false;
+                }
+                if (DBG) {
+                    Log.d(TAG, String.format("MDns service event code:%d id=%d", code, id));
+                }
+                switch (code) {
+                    case IMDnsEventListener.SERVICE_FOUND: {
+                        final DiscoveryInfo info = (DiscoveryInfo) obj;
+                        final String name = info.serviceName;
+                        final String type = info.registrationType;
+                        servInfo = new NsdServiceInfo(name, type);
+                        final int foundNetId = info.netId;
+                        if (foundNetId == 0L) {
+                            // Ignore services that do not have a Network: they are not usable
+                            // by apps, as they would need privileged permissions to use
+                            // interfaces that do not have an associated Network.
+                            break;
+                        }
+                        servInfo.setNetwork(new Network(foundNetId));
+                        clientInfo.onServiceFound(clientId, servInfo);
+                        break;
+                    }
+                    case IMDnsEventListener.SERVICE_LOST: {
+                        final DiscoveryInfo info = (DiscoveryInfo) obj;
+                        final String name = info.serviceName;
+                        final String type = info.registrationType;
+                        final int lostNetId = info.netId;
+                        servInfo = new NsdServiceInfo(name, type);
+                        // The network could be null if it was torn down when the service is lost
+                        // TODO: avoid returning null in that case, possibly by remembering found
+                        // services on the same interface index and their network at the time
+                        servInfo.setNetwork(lostNetId == 0 ? null : new Network(lostNetId));
+                        clientInfo.onServiceLost(clientId, servInfo);
+                        break;
+                    }
+                    case IMDnsEventListener.SERVICE_DISCOVERY_FAILED:
+                        clientInfo.onDiscoverServicesFailed(
+                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        break;
+                    case IMDnsEventListener.SERVICE_REGISTERED: {
+                        final RegistrationInfo info = (RegistrationInfo) obj;
+                        final String name = info.serviceName;
+                        servInfo = new NsdServiceInfo(name, null /* serviceType */);
+                        clientInfo.onRegisterServiceSucceeded(clientId, servInfo);
+                        break;
+                    }
+                    case IMDnsEventListener.SERVICE_REGISTRATION_FAILED:
+                        clientInfo.onRegisterServiceFailed(
+                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        break;
+                    case IMDnsEventListener.SERVICE_RESOLVED: {
+                        final ResolutionInfo info = (ResolutionInfo) obj;
+                        int index = 0;
+                        final String fullName = info.serviceFullName;
+                        while (index < fullName.length() && fullName.charAt(index) != '.') {
+                            if (fullName.charAt(index) == '\\') {
+                                ++index;
+                            }
+                            ++index;
+                        }
+                        if (index >= fullName.length()) {
+                            Log.e(TAG, "Invalid service found " + fullName);
+                            break;
+                        }
+
+                        String name = fullName.substring(0, index);
+                        String rest = fullName.substring(index);
+                        String type = rest.replace(".local.", "");
+
+                        clientInfo.mResolvedService.setServiceName(name);
+                        clientInfo.mResolvedService.setServiceType(type);
+                        clientInfo.mResolvedService.setPort(info.port);
+                        clientInfo.mResolvedService.setTxtRecords(info.txtRecord);
+                        // Network will be added after SERVICE_GET_ADDR_SUCCESS
+
+                        stopResolveService(id);
+                        removeRequestMap(clientId, id, clientInfo);
+
+                        final int id2 = getUniqueId();
+                        if (getAddrInfo(id2, info.hostname, info.interfaceIdx)) {
+                            storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE);
+                        } else {
+                            clientInfo.onResolveServiceFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            clientInfo.mResolvedService = null;
+                        }
+                        break;
+                    }
+                    case IMDnsEventListener.SERVICE_RESOLUTION_FAILED:
+                        /* NNN resolveId errorCode */
+                        stopResolveService(id);
+                        removeRequestMap(clientId, id, clientInfo);
+                        clientInfo.mResolvedService = null;
+                        clientInfo.onResolveServiceFailed(
+                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        break;
+                    case IMDnsEventListener.SERVICE_GET_ADDR_FAILED:
+                        /* NNN resolveId errorCode */
+                        stopGetAddrInfo(id);
+                        removeRequestMap(clientId, id, clientInfo);
+                        clientInfo.mResolvedService = null;
+                        clientInfo.onResolveServiceFailed(
+                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        break;
+                    case IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS: {
+                        /* NNN resolveId hostname ttl addr interfaceIdx netId */
+                        final GetAddressInfo info = (GetAddressInfo) obj;
+                        final String address = info.address;
+                        final int netId = info.netId;
+                        final Network network = netId == NETID_UNSET ? null : new Network(netId);
+                        InetAddress serviceHost = null;
+                        try {
+                            serviceHost = InetAddress.getByName(address);
+                        } catch (UnknownHostException e) {
+                            Log.wtf(TAG, "Invalid host in GET_ADDR_SUCCESS", e);
+                        }
+
+                        // If the resolved service is on an interface without a network, consider it
+                        // as a failure: it would not be usable by apps as they would need
+                        // privileged permissions.
+                        if (network != null && serviceHost != null) {
+                            clientInfo.mResolvedService.setHost(serviceHost);
+                            clientInfo.mResolvedService.setNetwork(network);
+                            clientInfo.onResolveServiceSucceeded(
+                                    clientId, clientInfo.mResolvedService);
+                        } else {
+                            clientInfo.onResolveServiceFailed(
+                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        }
+                        stopGetAddrInfo(id);
+                        removeRequestMap(clientId, id, clientInfo);
+                        clientInfo.mResolvedService = null;
+                        break;
+                    }
+                    default:
+                        return false;
+                }
+                return true;
+            }
+       }
+    }
+
+    @VisibleForTesting
+    NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
+        mCleanupDelayMs = cleanupDelayMs;
+        mContext = ctx;
+        mNsdStateMachine = new NsdStateMachine(TAG, handler);
+        mNsdStateMachine.start();
+        mMDnsManager = ctx.getSystemService(MDnsManager.class);
+        mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
+    }
+
+    public static NsdService create(Context context) {
+        HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
+        Handler handler = new Handler(thread.getLooper());
+        NsdService service = new NsdService(context, handler, CLEANUP_DELAY_MS);
+        return service;
+    }
+
+    private static class MDnsEventCallback extends IMDnsEventListener.Stub {
+        private final StateMachine mStateMachine;
+
+        MDnsEventCallback(StateMachine sm) {
+            mStateMachine = sm;
+        }
+
+        @Override
+        public void onServiceRegistrationStatus(final RegistrationInfo status) {
+            mStateMachine.sendMessage(
+                    MDNS_SERVICE_EVENT, status.result, status.id, status);
+        }
+
+        @Override
+        public void onServiceDiscoveryStatus(final DiscoveryInfo status) {
+            mStateMachine.sendMessage(
+                    MDNS_SERVICE_EVENT, status.result, status.id, status);
+        }
+
+        @Override
+        public void onServiceResolutionStatus(final ResolutionInfo status) {
+            mStateMachine.sendMessage(
+                    MDNS_SERVICE_EVENT, status.result, status.id, status);
+        }
+
+        @Override
+        public void onGettingServiceAddressStatus(final GetAddressInfo status) {
+            mStateMachine.sendMessage(
+                    MDNS_SERVICE_EVENT, status.result, status.id, status);
+        }
+
+        @Override
+        public int getInterfaceVersion() throws RemoteException {
+            return this.VERSION;
+        }
+
+        @Override
+        public String getInterfaceHash() throws RemoteException {
+            return this.HASH;
+        }
+    }
+
+    @Override
+    public INsdServiceConnector connect(INsdManagerCallback cb) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService");
+        final INsdServiceConnector connector = new NsdServiceConnector();
+        mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                NsdManager.REGISTER_CLIENT, new Pair<>(connector, cb)));
+        return connector;
+    }
+
+    private static class ListenerArgs {
+        public final NsdServiceConnector connector;
+        public final NsdServiceInfo serviceInfo;
+        ListenerArgs(NsdServiceConnector connector, NsdServiceInfo serviceInfo) {
+            this.connector = connector;
+            this.serviceInfo = serviceInfo;
+        }
+    }
+
+    private class NsdServiceConnector extends INsdServiceConnector.Stub
+            implements IBinder.DeathRecipient  {
+        @Override
+        public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.REGISTER_SERVICE, 0, listenerKey,
+                    new ListenerArgs(this, serviceInfo)));
+        }
+
+        @Override
+        public void unregisterService(int listenerKey) {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
+                    new ListenerArgs(this, null)));
+        }
+
+        @Override
+        public void discoverServices(int listenerKey, NsdServiceInfo serviceInfo) {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.DISCOVER_SERVICES, 0, listenerKey,
+                    new ListenerArgs(this, serviceInfo)));
+        }
+
+        @Override
+        public void stopDiscovery(int listenerKey) {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+        }
+
+        @Override
+        public void resolveService(int listenerKey, NsdServiceInfo serviceInfo) {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.RESOLVE_SERVICE, 0, listenerKey,
+                    new ListenerArgs(this, serviceInfo)));
+        }
+
+        @Override
+        public void startDaemon() {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+        }
+
+        @Override
+        public void binderDied() {
+            mNsdStateMachine.sendMessage(
+                    mNsdStateMachine.obtainMessage(NsdManager.UNREGISTER_CLIENT, this));
+        }
+    }
+
+    private void sendNsdStateChangeBroadcast(boolean isEnabled) {
+        final Intent intent = new Intent(NsdManager.ACTION_NSD_STATE_CHANGED);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        int nsdState = isEnabled ? NsdManager.NSD_STATE_ENABLED : NsdManager.NSD_STATE_DISABLED;
+        intent.putExtra(NsdManager.EXTRA_NSD_STATE, nsdState);
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private int getUniqueId() {
+        if (++mUniqueId == INVALID_ID) return ++mUniqueId;
+        return mUniqueId;
+    }
+
+    private boolean registerService(int regId, NsdServiceInfo service) {
+        if (DBG) {
+            Log.d(TAG, "registerService: " + regId + " " + service);
+        }
+        String name = service.getServiceName();
+        String type = service.getServiceType();
+        int port = service.getPort();
+        byte[] textRecord = service.getTxtRecord();
+        return mMDnsManager.registerService(regId, name, type, port, textRecord, IFACE_IDX_ANY);
+    }
+
+    private boolean unregisterService(int regId) {
+        return mMDnsManager.stopOperation(regId);
+    }
+
+    private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) {
+        final Network network = serviceInfo.getNetwork();
+        final String type = serviceInfo.getServiceType();
+        final int discoverInterface = getNetworkInterfaceIndex(network);
+        if (network != null && discoverInterface == IFACE_IDX_ANY) {
+            Log.e(TAG, "Interface to discover service on not found");
+            return false;
+        }
+        return mMDnsManager.discover(discoveryId, type, discoverInterface);
+    }
+
+    private boolean stopServiceDiscovery(int discoveryId) {
+        return mMDnsManager.stopOperation(discoveryId);
+    }
+
+    private boolean resolveService(int resolveId, NsdServiceInfo service) {
+        final String name = service.getServiceName();
+        final String type = service.getServiceType();
+        final Network network = service.getNetwork();
+        final int resolveInterface = getNetworkInterfaceIndex(network);
+        if (network != null && resolveInterface == IFACE_IDX_ANY) {
+            Log.e(TAG, "Interface to resolve service on not found");
+            return false;
+        }
+        return mMDnsManager.resolve(resolveId, name, type, "local.", resolveInterface);
+    }
+
+    /**
+     * Guess the interface to use to resolve or discover a service on a specific network.
+     *
+     * This is an imperfect guess, as for example the network may be gone or not yet fully
+     * registered. This is fine as failing is correct if the network is gone, and a client
+     * attempting to resolve/discover on a network not yet setup would have a bad time anyway; also
+     * this is to support the legacy mdnsresponder implementation, which historically resolved
+     * services on an unspecified network.
+     */
+    private int getNetworkInterfaceIndex(Network network) {
+        if (network == null) return IFACE_IDX_ANY;
+
+        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        if (cm == null) {
+            Log.wtf(TAG, "No ConnectivityManager for resolveService");
+            return IFACE_IDX_ANY;
+        }
+        final LinkProperties lp = cm.getLinkProperties(network);
+        if (lp == null) return IFACE_IDX_ANY;
+
+        // Only resolve on non-stacked interfaces
+        final NetworkInterface iface;
+        try {
+            iface = NetworkInterface.getByName(lp.getInterfaceName());
+        } catch (SocketException e) {
+            Log.e(TAG, "Error querying interface", e);
+            return IFACE_IDX_ANY;
+        }
+
+        if (iface == null) {
+            Log.e(TAG, "Interface not found: " + lp.getInterfaceName());
+            return IFACE_IDX_ANY;
+        }
+
+        return iface.getIndex();
+    }
+
+    private boolean stopResolveService(int resolveId) {
+        return mMDnsManager.stopOperation(resolveId);
+    }
+
+    private boolean getAddrInfo(int resolveId, String hostname, int interfaceIdx) {
+        return mMDnsManager.getServiceAddress(resolveId, hostname, interfaceIdx);
+    }
+
+    private boolean stopGetAddrInfo(int resolveId) {
+        return mMDnsManager.stopOperation(resolveId);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump " + TAG
+                    + " due to missing android.permission.DUMP permission");
+            return;
+        }
+
+        for (ClientInfo client : mClients.values()) {
+            pw.println("Client Info");
+            pw.println(client);
+        }
+
+        mNsdStateMachine.dump(fd, pw, args);
+    }
+
+    /* Information tracked per client */
+    private class ClientInfo {
+
+        private static final int MAX_LIMIT = 10;
+        private final INsdManagerCallback mCb;
+        /* Remembers a resolved service until getaddrinfo completes */
+        private NsdServiceInfo mResolvedService;
+
+        /* A map from client id to unique id sent to mDns */
+        private final SparseIntArray mClientIds = new SparseIntArray();
+
+        /* A map from client id to the type of the request we had received */
+        private final SparseIntArray mClientRequests = new SparseIntArray();
+
+        // The target SDK of this client < Build.VERSION_CODES.S
+        private boolean mIsLegacy = false;
+
+        private ClientInfo(INsdManagerCallback cb) {
+            mCb = cb;
+            if (DBG) Log.d(TAG, "New client");
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("mResolvedService ").append(mResolvedService).append("\n");
+            sb.append("mIsLegacy ").append(mIsLegacy).append("\n");
+            for(int i = 0; i< mClientIds.size(); i++) {
+                int clientID = mClientIds.keyAt(i);
+                sb.append("clientId ").append(clientID).
+                    append(" mDnsId ").append(mClientIds.valueAt(i)).
+                    append(" type ").append(mClientRequests.get(clientID)).append("\n");
+            }
+            return sb.toString();
+        }
+
+        private boolean isLegacy() {
+            return mIsLegacy;
+        }
+
+        private void setLegacy() {
+            mIsLegacy = true;
+        }
+
+        // Remove any pending requests from the global map when we get rid of a client,
+        // and send cancellations to the daemon.
+        private void expungeAllRequests() {
+            int globalId, clientId, i;
+            // TODO: to keep handler responsive, do not clean all requests for that client at once.
+            for (i = 0; i < mClientIds.size(); i++) {
+                clientId = mClientIds.keyAt(i);
+                globalId = mClientIds.valueAt(i);
+                mIdToClientInfoMap.remove(globalId);
+                if (DBG) {
+                    Log.d(TAG, "Terminating client-ID " + clientId
+                            + " global-ID " + globalId + " type " + mClientRequests.get(clientId));
+                }
+                switch (mClientRequests.get(clientId)) {
+                    case NsdManager.DISCOVER_SERVICES:
+                        stopServiceDiscovery(globalId);
+                        break;
+                    case NsdManager.RESOLVE_SERVICE:
+                        stopResolveService(globalId);
+                        break;
+                    case NsdManager.REGISTER_SERVICE:
+                        unregisterService(globalId);
+                        break;
+                    default:
+                        break;
+                }
+            }
+            mClientIds.clear();
+            mClientRequests.clear();
+        }
+
+        // mClientIds is a sparse array of listener id -> mDnsClient id.  For a given mDnsClient id,
+        // return the corresponding listener id.  mDnsClient id is also called a global id.
+        private int getClientId(final int globalId) {
+            int idx = mClientIds.indexOfValue(globalId);
+            if (idx < 0) {
+                return idx;
+            }
+            return mClientIds.keyAt(idx);
+        }
+
+        void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
+            try {
+                mCb.onDiscoverServicesStarted(listenerKey, info);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onDiscoverServicesStarted", e);
+            }
+        }
+
+        void onDiscoverServicesFailed(int listenerKey, int error) {
+            try {
+                mCb.onDiscoverServicesFailed(listenerKey, error);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onDiscoverServicesFailed", e);
+            }
+        }
+
+        void onServiceFound(int listenerKey, NsdServiceInfo info) {
+            try {
+                mCb.onServiceFound(listenerKey, info);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onServiceFound(", e);
+            }
+        }
+
+        void onServiceLost(int listenerKey, NsdServiceInfo info) {
+            try {
+                mCb.onServiceLost(listenerKey, info);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onServiceLost(", e);
+            }
+        }
+
+        void onStopDiscoveryFailed(int listenerKey, int error) {
+            try {
+                mCb.onStopDiscoveryFailed(listenerKey, error);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onStopDiscoveryFailed", e);
+            }
+        }
+
+        void onStopDiscoverySucceeded(int listenerKey) {
+            try {
+                mCb.onStopDiscoverySucceeded(listenerKey);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onStopDiscoverySucceeded", e);
+            }
+        }
+
+        void onRegisterServiceFailed(int listenerKey, int error) {
+            try {
+                mCb.onRegisterServiceFailed(listenerKey, error);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onRegisterServiceFailed", e);
+            }
+        }
+
+        void onRegisterServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+            try {
+                mCb.onRegisterServiceSucceeded(listenerKey, info);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onRegisterServiceSucceeded", e);
+            }
+        }
+
+        void onUnregisterServiceFailed(int listenerKey, int error) {
+            try {
+                mCb.onUnregisterServiceFailed(listenerKey, error);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onUnregisterServiceFailed", e);
+            }
+        }
+
+        void onUnregisterServiceSucceeded(int listenerKey) {
+            try {
+                mCb.onUnregisterServiceSucceeded(listenerKey);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onUnregisterServiceSucceeded", e);
+            }
+        }
+
+        void onResolveServiceFailed(int listenerKey, int error) {
+            try {
+                mCb.onResolveServiceFailed(listenerKey, error);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onResolveServiceFailed", e);
+            }
+        }
+
+        void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+            try {
+                mCb.onResolveServiceSucceeded(listenerKey, info);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onResolveServiceSucceeded", e);
+            }
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
new file mode 100644
index 0000000..6b623f4
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import android.annotation.Nullable;
+import android.net.IpConfiguration;
+import android.os.Environment;
+import android.util.ArrayMap;
+
+import com.android.server.net.IpConfigStore;
+
+
+/**
+ * This class provides an API to store and manage Ethernet network configuration.
+ */
+public class EthernetConfigStore {
+    private static final String ipConfigFile = Environment.getDataDirectory() +
+            "/misc/ethernet/ipconfig.txt";
+
+    private IpConfigStore mStore = new IpConfigStore();
+    private ArrayMap<String, IpConfiguration> mIpConfigurations;
+    private IpConfiguration mIpConfigurationForDefaultInterface;
+    private final Object mSync = new Object();
+
+    public EthernetConfigStore() {
+        mIpConfigurations = new ArrayMap<>(0);
+    }
+
+    public void read() {
+        synchronized (mSync) {
+            ArrayMap<String, IpConfiguration> configs =
+                    IpConfigStore.readIpConfigurations(ipConfigFile);
+
+            // This configuration may exist in old file versions when there was only a single active
+            // Ethernet interface.
+            if (configs.containsKey("0")) {
+                mIpConfigurationForDefaultInterface = configs.remove("0");
+            }
+
+            mIpConfigurations = configs;
+        }
+    }
+
+    public void write(String iface, IpConfiguration config) {
+        boolean modified;
+
+        synchronized (mSync) {
+            if (config == null) {
+                modified = mIpConfigurations.remove(iface) != null;
+            } else {
+                IpConfiguration oldConfig = mIpConfigurations.put(iface, config);
+                modified = !config.equals(oldConfig);
+            }
+
+            if (modified) {
+                mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations);
+            }
+        }
+    }
+
+    public ArrayMap<String, IpConfiguration> getIpConfigurations() {
+        synchronized (mSync) {
+            return new ArrayMap<>(mIpConfigurations);
+        }
+    }
+
+    @Nullable
+    public IpConfiguration getIpConfigurationForDefaultInterface() {
+        synchronized (mSync) {
+            return mIpConfigurationForDefaultInterface == null
+                    ? null : new IpConfiguration(mIpConfigurationForDefaultInterface);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java
new file mode 100644
index 0000000..57fbce7
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.os.Looper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+public class EthernetNetworkAgent extends NetworkAgent {
+
+    private static final String TAG = "EthernetNetworkAgent";
+
+    public interface Callbacks {
+        void onNetworkUnwanted();
+    }
+
+    private final Callbacks mCallbacks;
+
+    EthernetNetworkAgent(
+            @NonNull Context context,
+            @NonNull Looper looper,
+            @NonNull NetworkCapabilities nc,
+            @NonNull LinkProperties lp,
+            @NonNull NetworkAgentConfig config,
+            @Nullable NetworkProvider provider,
+            @NonNull Callbacks cb) {
+        super(context, looper, TAG, nc, lp, new NetworkScore.Builder().build(), config, provider);
+        mCallbacks = cb;
+    }
+
+    @Override
+    public void onNetworkUnwanted() {
+        mCallbacks.onNetworkUnwanted();
+    }
+
+    // sendLinkProperties is final in NetworkAgent, so it cannot be mocked.
+    public void sendLinkPropertiesImpl(LinkProperties lp) {
+        sendLinkProperties(lp);
+    }
+
+    public Callbacks getCallbacks() {
+        return mCallbacks;
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
new file mode 100644
index 0000000..eb22f78
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -0,0 +1,784 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityResources;
+import android.net.EthernetManager;
+import android.net.EthernetNetworkManagementException;
+import android.net.EthernetNetworkSpecifier;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkProperties;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkFactory;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.InterfaceParams;
+
+import java.io.FileDescriptor;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * {@link NetworkFactory} that represents Ethernet networks.
+ *
+ * This class reports a static network score of 70 when it is tracking an interface and that
+ * interface's link is up, and a score of 0 otherwise.
+ */
+public class EthernetNetworkFactory extends NetworkFactory {
+    private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
+    final static boolean DBG = true;
+
+    private final static int NETWORK_SCORE = 70;
+    private static final String NETWORK_TYPE = "Ethernet";
+    private static final String LEGACY_TCP_BUFFER_SIZES =
+            "524288,1048576,3145728,524288,1048576,2097152";
+
+    private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces =
+            new ConcurrentHashMap<>();
+    private final Handler mHandler;
+    private final Context mContext;
+    final Dependencies mDeps;
+
+    public static class Dependencies {
+        public void makeIpClient(Context context, String iface, IpClientCallbacks callbacks) {
+            IpClientUtil.makeIpClient(context, iface, callbacks);
+        }
+
+        public IpClientManager makeIpClientManager(@NonNull final IIpClient ipClient) {
+            return new IpClientManager(ipClient, TAG);
+        }
+
+        public EthernetNetworkAgent makeEthernetNetworkAgent(Context context, Looper looper,
+                NetworkCapabilities nc, LinkProperties lp, NetworkAgentConfig config,
+                NetworkProvider provider, EthernetNetworkAgent.Callbacks cb) {
+            return new EthernetNetworkAgent(context, looper, nc, lp, config, provider, cb);
+        }
+
+        public InterfaceParams getNetworkInterfaceByName(String name) {
+            return InterfaceParams.getByName(name);
+        }
+
+        // TODO: remove legacy resource fallback after migrating its overlays.
+        private String getPlatformTcpBufferSizes(Context context) {
+            final Resources r = context.getResources();
+            final int resId = r.getIdentifier("config_ethernet_tcp_buffers", "string",
+                    context.getPackageName());
+            return r.getString(resId);
+        }
+
+        public String getTcpBufferSizesFromResource(Context context) {
+            final String tcpBufferSizes;
+            final String platformTcpBufferSizes = getPlatformTcpBufferSizes(context);
+            if (!LEGACY_TCP_BUFFER_SIZES.equals(platformTcpBufferSizes)) {
+                // Platform resource is not the historical default: use the overlay.
+                tcpBufferSizes = platformTcpBufferSizes;
+            } else {
+                final ConnectivityResources resources = new ConnectivityResources(context);
+                tcpBufferSizes = resources.get().getString(R.string.config_ethernet_tcp_buffers);
+            }
+            return tcpBufferSizes;
+        }
+    }
+
+    public static class ConfigurationException extends AndroidRuntimeException {
+        public ConfigurationException(String msg) {
+            super(msg);
+        }
+    }
+
+    public EthernetNetworkFactory(Handler handler, Context context) {
+        this(handler, context, new Dependencies());
+    }
+
+    @VisibleForTesting
+    EthernetNetworkFactory(Handler handler, Context context, Dependencies deps) {
+        super(handler.getLooper(), context, NETWORK_TYPE, createDefaultNetworkCapabilities());
+
+        mHandler = handler;
+        mContext = context;
+        mDeps = deps;
+
+        setScoreFilter(NETWORK_SCORE);
+    }
+
+    @Override
+    public boolean acceptRequest(NetworkRequest request) {
+        if (DBG) {
+            Log.d(TAG, "acceptRequest, request: " + request);
+        }
+
+        return networkForRequest(request) != null;
+    }
+
+    @Override
+    protected void needNetworkFor(NetworkRequest networkRequest) {
+        NetworkInterfaceState network = networkForRequest(networkRequest);
+
+        if (network == null) {
+            Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest);
+            return;
+        }
+
+        if (++network.refCount == 1) {
+            network.start();
+        }
+    }
+
+    @Override
+    protected void releaseNetworkFor(NetworkRequest networkRequest) {
+        NetworkInterfaceState network = networkForRequest(networkRequest);
+        if (network == null) {
+            Log.e(TAG, "releaseNetworkFor, failed to get a network for " + networkRequest);
+            return;
+        }
+
+        if (--network.refCount == 0) {
+            network.stop();
+        }
+    }
+
+    /**
+     * Returns an array of available interface names. The array is sorted: unrestricted interfaces
+     * goes first, then sorted by name.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected String[] getAvailableInterfaces(boolean includeRestricted) {
+        return mTrackingInterfaces.values()
+                .stream()
+                .filter(iface -> !iface.isRestricted() || includeRestricted)
+                .sorted((iface1, iface2) -> {
+                    int r = Boolean.compare(iface1.isRestricted(), iface2.isRestricted());
+                    return r == 0 ? iface1.name.compareTo(iface2.name) : r;
+                })
+                .map(iface -> iface.name)
+                .toArray(String[]::new);
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void addInterface(@NonNull final String ifaceName, @NonNull final String hwAddress,
+            @NonNull final IpConfiguration ipConfig,
+            @NonNull final NetworkCapabilities capabilities) {
+        if (mTrackingInterfaces.containsKey(ifaceName)) {
+            Log.e(TAG, "Interface with name " + ifaceName + " already exists.");
+            return;
+        }
+
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder(capabilities)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))
+                .build();
+
+        if (DBG) {
+            Log.d(TAG, "addInterface, iface: " + ifaceName + ", capabilities: " + nc);
+        }
+
+        final NetworkInterfaceState iface = new NetworkInterfaceState(
+                ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, this, mDeps);
+        mTrackingInterfaces.put(ifaceName, iface);
+        updateCapabilityFilter();
+    }
+
+    @VisibleForTesting
+    protected int getInterfaceState(@NonNull String iface) {
+        final NetworkInterfaceState interfaceState = mTrackingInterfaces.get(iface);
+        if (interfaceState == null) {
+            return EthernetManager.STATE_ABSENT;
+        } else if (!interfaceState.mLinkUp) {
+            return EthernetManager.STATE_LINK_DOWN;
+        } else {
+            return EthernetManager.STATE_LINK_UP;
+        }
+    }
+
+    /**
+     * Update a network's configuration and restart it if necessary.
+     *
+     * @param ifaceName the interface name of the network to be updated.
+     * @param ipConfig the desired {@link IpConfiguration} for the given network or null. If
+     *                 {@code null} is passed, the existing IpConfiguration is not updated.
+     * @param capabilities the desired {@link NetworkCapabilities} for the given network. If
+     *                     {@code null} is passed, then the network's current
+     *                     {@link NetworkCapabilities} will be used in support of existing APIs as
+     *                     the public API does not allow this.
+     * @param listener an optional {@link INetworkInterfaceOutcomeReceiver} to notify callers of
+     *                 completion.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void updateInterface(@NonNull final String ifaceName,
+            @Nullable final IpConfiguration ipConfig,
+            @Nullable final NetworkCapabilities capabilities,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (!hasInterface(ifaceName)) {
+            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
+            return;
+        }
+
+        final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+        iface.updateInterface(ipConfig, capabilities, listener);
+        mTrackingInterfaces.put(ifaceName, iface);
+        updateCapabilityFilter();
+    }
+
+    private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
+            NetworkCapabilities addedNc) {
+       final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(nc);
+       for (int transport : addedNc.getTransportTypes()) builder.addTransportType(transport);
+       for (int capability : addedNc.getCapabilities()) builder.addCapability(capability);
+       return builder.build();
+    }
+
+    private void updateCapabilityFilter() {
+        NetworkCapabilities capabilitiesFilter = createDefaultNetworkCapabilities();
+        for (NetworkInterfaceState iface:  mTrackingInterfaces.values()) {
+            capabilitiesFilter = mixInCapabilities(capabilitiesFilter, iface.mCapabilities);
+        }
+
+        if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter);
+        setCapabilityFilter(capabilitiesFilter);
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilities() {
+        return NetworkCapabilities.Builder
+                .withoutDefaultCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET).build();
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void removeInterface(String interfaceName) {
+        NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
+        if (iface != null) {
+            iface.maybeSendNetworkManagementCallbackForAbort();
+            iface.stop();
+        }
+
+        updateCapabilityFilter();
+    }
+
+    /** Returns true if state has been modified */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (!hasInterface(ifaceName)) {
+            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
+            return false;
+        }
+
+        if (DBG) {
+            Log.d(TAG, "updateInterfaceLinkState, iface: " + ifaceName + ", up: " + up);
+        }
+
+        NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+        return iface.updateLinkState(up, listener);
+    }
+
+    private void maybeSendNetworkManagementCallbackForUntracked(
+            String ifaceName, INetworkInterfaceOutcomeReceiver listener) {
+        maybeSendNetworkManagementCallback(listener, null,
+                new EthernetNetworkManagementException(
+                        ifaceName + " can't be updated as it is not available."));
+    }
+
+    @VisibleForTesting
+    protected boolean hasInterface(String ifaceName) {
+        return mTrackingInterfaces.containsKey(ifaceName);
+    }
+
+    private NetworkInterfaceState networkForRequest(NetworkRequest request) {
+        String requestedIface = null;
+
+        NetworkSpecifier specifier = request.getNetworkSpecifier();
+        if (specifier instanceof EthernetNetworkSpecifier) {
+            requestedIface = ((EthernetNetworkSpecifier) specifier)
+                .getInterfaceName();
+        }
+
+        NetworkInterfaceState network = null;
+        if (!TextUtils.isEmpty(requestedIface)) {
+            NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface);
+            if (n != null && request.canBeSatisfiedBy(n.mCapabilities)) {
+                network = n;
+            }
+        } else {
+            for (NetworkInterfaceState n : mTrackingInterfaces.values()) {
+                if (request.canBeSatisfiedBy(n.mCapabilities) && n.mLinkUp) {
+                    network = n;
+                    break;
+                }
+            }
+        }
+
+        if (DBG) {
+            Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network);
+        }
+
+        return network;
+    }
+
+    private static void maybeSendNetworkManagementCallback(
+            @Nullable final INetworkInterfaceOutcomeReceiver listener,
+            @Nullable final String iface,
+            @Nullable final EthernetNetworkManagementException e) {
+        if (null == listener) {
+            return;
+        }
+
+        try {
+            if (iface != null) {
+                listener.onResult(iface);
+            } else {
+                listener.onError(e);
+            }
+        } catch (RemoteException re) {
+            Log.e(TAG, "Can't send onComplete for network management callback", re);
+        }
+    }
+
+    @VisibleForTesting
+    static class NetworkInterfaceState {
+        final String name;
+
+        private final String mHwAddress;
+        private final Handler mHandler;
+        private final Context mContext;
+        private final NetworkFactory mNetworkFactory;
+        private final Dependencies mDeps;
+
+        private static String sTcpBufferSizes = null;  // Lazy initialized.
+
+        private boolean mLinkUp;
+        private int mLegacyType;
+        private LinkProperties mLinkProperties = new LinkProperties();
+
+        private volatile @Nullable IpClientManager mIpClient;
+        private @NonNull NetworkCapabilities mCapabilities;
+        private @Nullable EthernetIpClientCallback mIpClientCallback;
+        private @Nullable EthernetNetworkAgent mNetworkAgent;
+        private @Nullable IpConfiguration mIpConfig;
+
+        /**
+         * A map of TRANSPORT_* types to legacy transport types available for each type an ethernet
+         * interface could propagate.
+         *
+         * There are no legacy type equivalents to LOWPAN or WIFI_AWARE. These types are set to
+         * TYPE_NONE to match the behavior of their own network factories.
+         */
+        private static final SparseArray<Integer> sTransports = new SparseArray();
+        static {
+            sTransports.put(NetworkCapabilities.TRANSPORT_ETHERNET,
+                    ConnectivityManager.TYPE_ETHERNET);
+            sTransports.put(NetworkCapabilities.TRANSPORT_BLUETOOTH,
+                    ConnectivityManager.TYPE_BLUETOOTH);
+            sTransports.put(NetworkCapabilities.TRANSPORT_WIFI, ConnectivityManager.TYPE_WIFI);
+            sTransports.put(NetworkCapabilities.TRANSPORT_CELLULAR,
+                    ConnectivityManager.TYPE_MOBILE);
+            sTransports.put(NetworkCapabilities.TRANSPORT_LOWPAN, ConnectivityManager.TYPE_NONE);
+            sTransports.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+                    ConnectivityManager.TYPE_NONE);
+        }
+
+        long refCount = 0;
+
+        private class EthernetIpClientCallback extends IpClientCallbacks {
+            private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
+            private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
+            @Nullable INetworkInterfaceOutcomeReceiver mNetworkManagementListener;
+
+            EthernetIpClientCallback(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+                mNetworkManagementListener = listener;
+            }
+
+            @Override
+            public void onIpClientCreated(IIpClient ipClient) {
+                mIpClient = mDeps.makeIpClientManager(ipClient);
+                mIpClientStartCv.open();
+            }
+
+            private void awaitIpClientStart() {
+                mIpClientStartCv.block();
+            }
+
+            private void awaitIpClientShutdown() {
+                mIpClientShutdownCv.block();
+            }
+
+            // At the time IpClient is stopped, an IpClient event may have already been posted on
+            // the back of the handler and is awaiting execution. Once that event is executed, the
+            // associated callback object may not be valid anymore
+            // (NetworkInterfaceState#mIpClientCallback points to a different object / null).
+            private boolean isCurrentCallback() {
+                return this == mIpClientCallback;
+            }
+
+            private void handleIpEvent(final @NonNull Runnable r) {
+                mHandler.post(() -> {
+                    if (!isCurrentCallback()) {
+                        Log.i(TAG, "Ignoring stale IpClientCallbacks " + this);
+                        return;
+                    }
+                    r.run();
+                });
+            }
+
+            @Override
+            public void onProvisioningSuccess(LinkProperties newLp) {
+                handleIpEvent(() -> onIpLayerStarted(newLp, mNetworkManagementListener));
+            }
+
+            @Override
+            public void onProvisioningFailure(LinkProperties newLp) {
+                // This cannot happen due to provisioning timeout, because our timeout is 0. It can
+                // happen due to errors while provisioning or on provisioning loss.
+                handleIpEvent(() -> onIpLayerStopped(mNetworkManagementListener));
+            }
+
+            @Override
+            public void onLinkPropertiesChange(LinkProperties newLp) {
+                handleIpEvent(() -> updateLinkProperties(newLp));
+            }
+
+            @Override
+            public void onReachabilityLost(String logMsg) {
+                handleIpEvent(() -> updateNeighborLostEvent(logMsg));
+            }
+
+            @Override
+            public void onQuit() {
+                mIpClient = null;
+                mIpClientShutdownCv.open();
+            }
+        }
+
+        NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
+                @NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities,
+                NetworkFactory networkFactory, Dependencies deps) {
+            name = ifaceName;
+            mIpConfig = Objects.requireNonNull(ipConfig);
+            mCapabilities = Objects.requireNonNull(capabilities);
+            mLegacyType = getLegacyType(mCapabilities);
+            mHandler = handler;
+            mContext = context;
+            mNetworkFactory = networkFactory;
+            mDeps = deps;
+            mHwAddress = hwAddress;
+        }
+
+        /**
+         * Determines the legacy transport type from a NetworkCapabilities transport type. Defaults
+         * to legacy TYPE_NONE if there is no known conversion
+         */
+        private static int getLegacyType(int transport) {
+            return sTransports.get(transport, ConnectivityManager.TYPE_NONE);
+        }
+
+        private static int getLegacyType(@NonNull final NetworkCapabilities capabilities) {
+            final int[] transportTypes = capabilities.getTransportTypes();
+            if (transportTypes.length > 0) {
+                return getLegacyType(transportTypes[0]);
+            }
+
+            // Should never happen as transport is always one of ETHERNET or a valid override
+            throw new ConfigurationException("Network Capabilities do not have an associated "
+                    + "transport type.");
+        }
+
+        private void setCapabilities(@NonNull final NetworkCapabilities capabilities) {
+            mCapabilities = new NetworkCapabilities(capabilities);
+            mLegacyType = getLegacyType(mCapabilities);
+        }
+
+        void updateInterface(@Nullable final IpConfiguration ipConfig,
+                @Nullable final NetworkCapabilities capabilities,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (DBG) {
+                Log.d(TAG, "updateInterface, iface: " + name
+                        + ", ipConfig: " + ipConfig + ", old ipConfig: " + mIpConfig
+                        + ", capabilities: " + capabilities + ", old capabilities: " + mCapabilities
+                        + ", listener: " + listener
+                );
+            }
+
+            if (null != ipConfig){
+                mIpConfig = ipConfig;
+            }
+            if (null != capabilities) {
+                setCapabilities(capabilities);
+            }
+            // Send an abort callback if a request is filed before the previous one has completed.
+            maybeSendNetworkManagementCallbackForAbort();
+            // TODO: Update this logic to only do a restart if required. Although a restart may
+            //  be required due to the capabilities or ipConfiguration values, not all
+            //  capabilities changes require a restart.
+            restart(listener);
+        }
+
+        boolean isRestricted() {
+            return !mCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        }
+
+        private void start() {
+            start(null);
+        }
+
+        private void start(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mIpClient != null) {
+                if (DBG) Log.d(TAG, "IpClient already started");
+                return;
+            }
+            if (DBG) {
+                Log.d(TAG, String.format("Starting Ethernet IpClient(%s)", name));
+            }
+
+            mIpClientCallback = new EthernetIpClientCallback(listener);
+            mDeps.makeIpClient(mContext, name, mIpClientCallback);
+            mIpClientCallback.awaitIpClientStart();
+
+            if (sTcpBufferSizes == null) {
+                sTcpBufferSizes = mDeps.getTcpBufferSizesFromResource(mContext);
+            }
+            provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes);
+        }
+
+        void onIpLayerStarted(@NonNull final LinkProperties linkProperties,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mNetworkAgent != null) {
+                Log.e(TAG, "Already have a NetworkAgent - aborting new request");
+                stop();
+                return;
+            }
+            mLinkProperties = linkProperties;
+
+            // Create our NetworkAgent.
+            final NetworkAgentConfig config = new NetworkAgentConfig.Builder()
+                    .setLegacyType(mLegacyType)
+                    .setLegacyTypeName(NETWORK_TYPE)
+                    .setLegacyExtraInfo(mHwAddress)
+                    .build();
+            mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(),
+                    mCapabilities, mLinkProperties, config, mNetworkFactory.getProvider(),
+                    new EthernetNetworkAgent.Callbacks() {
+                        @Override
+                        public void onNetworkUnwanted() {
+                            // if mNetworkAgent is null, we have already called stop.
+                            if (mNetworkAgent == null) return;
+
+                            if (this == mNetworkAgent.getCallbacks()) {
+                                stop();
+                            } else {
+                                Log.d(TAG, "Ignoring unwanted as we have a more modern " +
+                                        "instance");
+                            }
+                        }
+                    });
+            mNetworkAgent.register();
+            mNetworkAgent.markConnected();
+            realizeNetworkManagementCallback(name, null);
+        }
+
+        void onIpLayerStopped(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            // There is no point in continuing if the interface is gone as stop() will be triggered
+            // by removeInterface() when processed on the handler thread and start() won't
+            // work for a non-existent interface.
+            if (null == mDeps.getNetworkInterfaceByName(name)) {
+                if (DBG) Log.d(TAG, name + " is no longer available.");
+                // Send a callback in case a provisioning request was in progress.
+                maybeSendNetworkManagementCallbackForAbort();
+                return;
+            }
+            restart(listener);
+        }
+
+        private void maybeSendNetworkManagementCallbackForAbort() {
+            realizeNetworkManagementCallback(null,
+                    new EthernetNetworkManagementException(
+                            "The IP provisioning request has been aborted."));
+        }
+
+        // Must be called on the handler thread
+        private void realizeNetworkManagementCallback(@Nullable final String iface,
+                @Nullable final EthernetNetworkManagementException e) {
+            ensureRunningOnEthernetHandlerThread();
+            if (null == mIpClientCallback) {
+                return;
+            }
+
+            EthernetNetworkFactory.maybeSendNetworkManagementCallback(
+                    mIpClientCallback.mNetworkManagementListener, iface, e);
+            // Only send a single callback per listener.
+            mIpClientCallback.mNetworkManagementListener = null;
+        }
+
+        private void ensureRunningOnEthernetHandlerThread() {
+            if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+                throw new IllegalStateException(
+                        "Not running on the Ethernet thread: "
+                                + Thread.currentThread().getName());
+            }
+        }
+
+        void updateLinkProperties(LinkProperties linkProperties) {
+            mLinkProperties = linkProperties;
+            if (mNetworkAgent != null) {
+                mNetworkAgent.sendLinkPropertiesImpl(linkProperties);
+            }
+        }
+
+        void updateNeighborLostEvent(String logMsg) {
+            Log.i(TAG, "updateNeighborLostEvent " + logMsg);
+            // Reachability lost will be seen only if the gateway is not reachable.
+            // Since ethernet FW doesn't have the mechanism to scan for new networks
+            // like WiFi, simply restart.
+            // If there is a better network, that will become default and apps
+            // will be able to use internet. If ethernet gets connected again,
+            // and has backhaul connectivity, it will become default.
+            restart();
+        }
+
+        /** Returns true if state has been modified */
+        boolean updateLinkState(final boolean up,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mLinkUp == up)  {
+                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, null,
+                        new EthernetNetworkManagementException(
+                                "No changes with requested link state " + up + " for " + name));
+                return false;
+            }
+            mLinkUp = up;
+
+            if (!up) { // was up, goes down
+                // Send an abort on a provisioning request callback if necessary before stopping.
+                maybeSendNetworkManagementCallbackForAbort();
+                stop();
+                // If only setting the interface down, send a callback to signal completion.
+                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
+            } else { // was down, goes up
+                stop();
+                start(listener);
+            }
+
+            return true;
+        }
+
+        void stop() {
+            // Invalidate all previous start requests
+            if (mIpClient != null) {
+                mIpClient.shutdown();
+                mIpClientCallback.awaitIpClientShutdown();
+                mIpClient = null;
+            }
+            mIpClientCallback = null;
+
+            if (mNetworkAgent != null) {
+                mNetworkAgent.unregister();
+                mNetworkAgent = null;
+            }
+            mLinkProperties.clear();
+        }
+
+        private static void provisionIpClient(@NonNull final IpClientManager ipClient,
+                @NonNull final IpConfiguration config, @NonNull final String tcpBufferSizes) {
+            if (config.getProxySettings() == ProxySettings.STATIC ||
+                    config.getProxySettings() == ProxySettings.PAC) {
+                ipClient.setHttpProxy(config.getHttpProxy());
+            }
+
+            if (!TextUtils.isEmpty(tcpBufferSizes)) {
+                ipClient.setTcpBufferSizes(tcpBufferSizes);
+            }
+
+            ipClient.startProvisioning(createProvisioningConfiguration(config));
+        }
+
+        private static ProvisioningConfiguration createProvisioningConfiguration(
+                @NonNull final IpConfiguration config) {
+            if (config.getIpAssignment() == IpAssignment.STATIC) {
+                return new ProvisioningConfiguration.Builder()
+                        .withStaticConfiguration(config.getStaticIpConfiguration())
+                        .build();
+            }
+            return new ProvisioningConfiguration.Builder()
+                        .withProvisioningTimeoutMs(0)
+                        .build();
+        }
+
+        void restart() {
+            restart(null);
+        }
+
+        void restart(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (DBG) Log.d(TAG, "reconnecting Ethernet");
+            stop();
+            start(listener);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "{ "
+                    + "refCount: " + refCount + ", "
+                    + "iface: " + name + ", "
+                    + "up: " + mLinkUp + ", "
+                    + "hwAddress: " + mHwAddress + ", "
+                    + "networkCapabilities: " + mCapabilities + ", "
+                    + "networkAgent: " + mNetworkAgent + ", "
+                    + "ipClient: " + mIpClient + ","
+                    + "linkProperties: " + mLinkProperties
+                    + "}";
+        }
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
+        super.dump(fd, pw, args);
+        pw.println(getClass().getSimpleName());
+        pw.println("Tracking interfaces:");
+        pw.increaseIndent();
+        for (String iface: mTrackingInterfaces.keySet()) {
+            NetworkInterfaceState ifaceState = mTrackingInterfaces.get(iface);
+            pw.println(iface + ":" + ifaceState);
+            pw.increaseIndent();
+            if (null == ifaceState.mIpClient) {
+                pw.println("IpClient is null");
+            }
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetService.java b/service-t/src/com/android/server/ethernet/EthernetService.java
new file mode 100644
index 0000000..d405fd5
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import android.content.Context;
+import android.net.INetd;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+
+import java.util.Objects;
+
+// TODO: consider renaming EthernetServiceImpl to EthernetService and deleting this file.
+public final class EthernetService {
+    private static final String TAG = "EthernetService";
+    private static final String THREAD_NAME = "EthernetServiceThread";
+
+    private static INetd getNetd(Context context) {
+        final INetd netd =
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE));
+        Objects.requireNonNull(netd, "could not get netd instance");
+        return netd;
+    }
+
+    public static EthernetServiceImpl create(Context context) {
+        final HandlerThread handlerThread = new HandlerThread(THREAD_NAME);
+        handlerThread.start();
+        final Handler handler = new Handler(handlerThread.getLooper());
+        final EthernetNetworkFactory factory = new EthernetNetworkFactory(handler, context);
+        return new EthernetServiceImpl(context, handler,
+                new EthernetTracker(context, handler, factory, getNetd(context)));
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
new file mode 100644
index 0000000..5e830ad
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.IEthernetManager;
+import android.net.IEthernetServiceListener;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.ITetheredInterfaceCallback;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.PrintWriterPrinter;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * EthernetServiceImpl handles remote Ethernet operation requests by implementing
+ * the IEthernetManager interface.
+ */
+public class EthernetServiceImpl extends IEthernetManager.Stub {
+    private static final String TAG = "EthernetServiceImpl";
+
+    @VisibleForTesting
+    final AtomicBoolean mStarted = new AtomicBoolean(false);
+    private final Context mContext;
+    private final Handler mHandler;
+    private final EthernetTracker mTracker;
+
+    EthernetServiceImpl(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetTracker tracker) {
+        mContext = context;
+        mHandler = handler;
+        mTracker = tracker;
+    }
+
+    private void enforceAutomotiveDevice(final @NonNull String methodName) {
+        PermissionUtils.enforceSystemFeature(mContext, PackageManager.FEATURE_AUTOMOTIVE,
+                methodName + " is only available on automotive devices.");
+    }
+
+    private boolean checkUseRestrictedNetworksPermission() {
+        return PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+    }
+
+    public void start() {
+        Log.i(TAG, "Starting Ethernet service");
+        mTracker.start();
+        mStarted.set(true);
+    }
+
+    private void throwIfEthernetNotStarted() {
+        if (!mStarted.get()) {
+            throw new IllegalStateException("System isn't ready to change ethernet configurations");
+        }
+    }
+
+    @Override
+    public String[] getAvailableInterfaces() throws RemoteException {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        return mTracker.getInterfaces(checkUseRestrictedNetworksPermission());
+    }
+
+    /**
+     * Get Ethernet configuration
+     * @return the Ethernet Configuration, contained in {@link IpConfiguration}.
+     */
+    @Override
+    public IpConfiguration getConfiguration(String iface) {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        return new IpConfiguration(mTracker.getIpConfiguration(iface));
+    }
+
+    /**
+     * Set Ethernet configuration
+     */
+    @Override
+    public void setConfiguration(String iface, IpConfiguration config) {
+        throwIfEthernetNotStarted();
+
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        // TODO: this does not check proxy settings, gateways, etc.
+        // Fix this by making IpConfiguration a complete representation of static configuration.
+        mTracker.updateIpConfiguration(iface, new IpConfiguration(config));
+    }
+
+    /**
+     * Indicates whether given interface is available.
+     */
+    @Override
+    public boolean isAvailable(String iface) {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        return mTracker.isTrackingInterface(iface);
+    }
+
+    /**
+     * Adds a listener.
+     * @param listener A {@link IEthernetServiceListener} to add.
+     */
+    public void addListener(IEthernetServiceListener listener) throws RemoteException {
+        Objects.requireNonNull(listener, "listener must not be null");
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        mTracker.addListener(listener, checkUseRestrictedNetworksPermission());
+    }
+
+    /**
+     * Removes a listener.
+     * @param listener A {@link IEthernetServiceListener} to remove.
+     */
+    public void removeListener(IEthernetServiceListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        mTracker.removeListener(listener);
+    }
+
+    @Override
+    public void setIncludeTestInterfaces(boolean include) {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.setIncludeTestInterfaces(include);
+    }
+
+    @Override
+    public void requestTetheredInterface(ITetheredInterfaceCallback callback) {
+        Objects.requireNonNull(callback, "callback must not be null");
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.requestTetheredInterface(callback);
+    }
+
+    @Override
+    public void releaseTetheredInterface(ITetheredInterfaceCallback callback) {
+        Objects.requireNonNull(callback, "callback must not be null");
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.releaseTetheredInterface(callback);
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump EthernetService from pid="
+                    + Binder.getCallingPid()
+                    + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+
+        pw.println("Current Ethernet state: ");
+        pw.increaseIndent();
+        mTracker.dump(fd, pw, args);
+        pw.decreaseIndent();
+
+        pw.println("Handler:");
+        pw.increaseIndent();
+        mHandler.dump(new PrintWriterPrinter(pw), "EthernetServiceImpl");
+        pw.decreaseIndent();
+    }
+
+    private void enforceNetworkManagementPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_ETHERNET_NETWORKS,
+                "EthernetServiceImpl");
+    }
+
+    private void enforceManageTestNetworksPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_TEST_NETWORKS,
+                "EthernetServiceImpl");
+    }
+
+    private void maybeValidateTestCapabilities(final String iface,
+            @Nullable final NetworkCapabilities nc) {
+        if (!mTracker.isValidTestInterface(iface)) {
+            return;
+        }
+        // For test interfaces, only null or capabilities that include TRANSPORT_TEST are
+        // allowed.
+        if (nc != null && !nc.hasTransport(TRANSPORT_TEST)) {
+            throw new IllegalArgumentException(
+                    "Updates to test interfaces must have NetworkCapabilities.TRANSPORT_TEST.");
+        }
+    }
+
+    private void enforceAdminPermission(final String iface, boolean enforceAutomotive,
+            final String logMessage) {
+        if (mTracker.isValidTestInterface(iface)) {
+            enforceManageTestNetworksPermission();
+        } else {
+            enforceNetworkManagementPermission();
+            if (enforceAutomotive) {
+                enforceAutomotiveDevice(logMessage);
+            }
+        }
+    }
+
+    @Override
+    public void updateConfiguration(@NonNull final String iface,
+            @NonNull final EthernetNetworkUpdateRequest request,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Objects.requireNonNull(iface);
+        Objects.requireNonNull(request);
+        throwIfEthernetNotStarted();
+
+        // TODO: validate that iface is listed in overlay config_ethernet_interfaces
+        // only automotive devices are allowed to set the NetworkCapabilities using this API
+        enforceAdminPermission(iface, request.getNetworkCapabilities() != null,
+                "updateConfiguration() with non-null capabilities");
+        maybeValidateTestCapabilities(iface, request.getNetworkCapabilities());
+
+        mTracker.updateConfiguration(
+                iface, request.getIpConfiguration(), request.getNetworkCapabilities(), listener);
+    }
+
+    @Override
+    public void connectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Log.i(TAG, "connectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Objects.requireNonNull(iface);
+        throwIfEthernetNotStarted();
+
+        enforceAdminPermission(iface, true, "connectNetwork()");
+
+        mTracker.connectNetwork(iface, listener);
+    }
+
+    @Override
+    public void disconnectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Log.i(TAG, "disconnectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Objects.requireNonNull(iface);
+        throwIfEthernetNotStarted();
+
+        enforceAdminPermission(iface, true, "connectNetwork()");
+
+        mTracker.disconnectNetwork(iface, listener);
+    }
+
+    @Override
+    public void setEthernetEnabled(boolean enabled) {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+
+        mTracker.setEthernetEnabled(enabled);
+    }
+
+    @Override
+    public List<String> getInterfaceList() {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        return mTracker.getInterfaceList();
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
new file mode 100644
index 0000000..693d91a
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -0,0 +1,966 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static android.net.EthernetManager.ETHERNET_STATE_DISABLED;
+import static android.net.EthernetManager.ETHERNET_STATE_ENABLED;
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityResources;
+import android.net.EthernetManager;
+import android.net.IEthernetServiceListener;
+import android.net.INetd;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.ITetheredInterfaceCallback;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tracks Ethernet interfaces and manages interface configurations.
+ *
+ * <p>Interfaces may have different {@link android.net.NetworkCapabilities}. This mapping is defined
+ * in {@code config_ethernet_interfaces}. Notably, some interfaces could be marked as restricted by
+ * not specifying {@link android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED} flag.
+ * Interfaces could have associated {@link android.net.IpConfiguration}.
+ * Ethernet Interfaces may be present at boot time or appear after boot (e.g., for Ethernet adapters
+ * connected over USB). This class supports multiple interfaces. When an interface appears on the
+ * system (or is present at boot time) this class will start tracking it and bring it up. Only
+ * interfaces whose names match the {@code config_ethernet_iface_regex} regular expression are
+ * tracked.
+ *
+ * <p>All public or package private methods must be thread-safe unless stated otherwise.
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public class EthernetTracker {
+    private static final int INTERFACE_MODE_CLIENT = 1;
+    private static final int INTERFACE_MODE_SERVER = 2;
+
+    private static final String TAG = EthernetTracker.class.getSimpleName();
+    private static final boolean DBG = EthernetNetworkFactory.DBG;
+
+    private static final String TEST_IFACE_REGEXP = TEST_TAP_PREFIX + "\\d+";
+    private static final String LEGACY_IFACE_REGEXP = "eth\\d";
+
+    /**
+     * Interface names we track. This is a product-dependent regular expression, plus,
+     * if setIncludeTestInterfaces is true, any test interfaces.
+     */
+    private volatile String mIfaceMatch;
+    /**
+     * Track test interfaces if true, don't track otherwise.
+     */
+    private boolean mIncludeTestInterfaces = false;
+
+    /** Mapping between {iface name | mac address} -> {NetworkCapabilities} */
+    private final ConcurrentHashMap<String, NetworkCapabilities> mNetworkCapabilities =
+            new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<String, IpConfiguration> mIpConfigurations =
+            new ConcurrentHashMap<>();
+
+    private final Context mContext;
+    private final INetd mNetd;
+    private final Handler mHandler;
+    private final EthernetNetworkFactory mFactory;
+    private final EthernetConfigStore mConfigStore;
+    private final Dependencies mDeps;
+
+    private final RemoteCallbackList<IEthernetServiceListener> mListeners =
+            new RemoteCallbackList<>();
+    private final TetheredInterfaceRequestList mTetheredInterfaceRequests =
+            new TetheredInterfaceRequestList();
+
+    // Used only on the handler thread
+    private String mDefaultInterface;
+    private int mDefaultInterfaceMode = INTERFACE_MODE_CLIENT;
+    // Tracks whether clients were notified that the tethered interface is available
+    private boolean mTetheredInterfaceWasAvailable = false;
+    private volatile IpConfiguration mIpConfigForDefaultInterface;
+
+    private int mEthernetState = ETHERNET_STATE_ENABLED;
+
+    private class TetheredInterfaceRequestList extends
+            RemoteCallbackList<ITetheredInterfaceCallback> {
+        @Override
+        public void onCallbackDied(ITetheredInterfaceCallback cb, Object cookie) {
+            mHandler.post(EthernetTracker.this::maybeUntetherDefaultInterface);
+        }
+    }
+
+    public static class Dependencies {
+        // TODO: remove legacy resource fallback after migrating its overlays.
+        private String getPlatformRegexResource(Context context) {
+            final Resources r = context.getResources();
+            final int resId =
+                r.getIdentifier("config_ethernet_iface_regex", "string", context.getPackageName());
+            return r.getString(resId);
+        }
+
+        // TODO: remove legacy resource fallback after migrating its overlays.
+        private String[] getPlatformInterfaceConfigs(Context context) {
+            final Resources r = context.getResources();
+            final int resId = r.getIdentifier("config_ethernet_interfaces", "array",
+                    context.getPackageName());
+            return r.getStringArray(resId);
+        }
+
+        public String getInterfaceRegexFromResource(Context context) {
+            final String platformRegex = getPlatformRegexResource(context);
+            final String match;
+            if (!LEGACY_IFACE_REGEXP.equals(platformRegex)) {
+                // Platform resource is not the historical default: use the overlay
+                match = platformRegex;
+            } else {
+                final ConnectivityResources resources = new ConnectivityResources(context);
+                match = resources.get().getString(
+                        com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
+            }
+            return match;
+        }
+
+        public String[] getInterfaceConfigFromResource(Context context) {
+            final String[] platformInterfaceConfigs = getPlatformInterfaceConfigs(context);
+            final String[] interfaceConfigs;
+            if (platformInterfaceConfigs.length != 0) {
+                // Platform resource is not the historical default: use the overlay
+                interfaceConfigs = platformInterfaceConfigs;
+            } else {
+                final ConnectivityResources resources = new ConnectivityResources(context);
+                interfaceConfigs = resources.get().getStringArray(
+                        com.android.connectivity.resources.R.array.config_ethernet_interfaces);
+            }
+            return interfaceConfigs;
+        }
+    }
+
+    EthernetTracker(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd) {
+        this(context, handler, factory, netd, new Dependencies());
+    }
+
+    @VisibleForTesting
+    EthernetTracker(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd,
+            @NonNull final Dependencies deps) {
+        mContext = context;
+        mHandler = handler;
+        mFactory = factory;
+        mNetd = netd;
+        mDeps = deps;
+
+        // Interface match regex.
+        updateIfaceMatchRegexp();
+
+        // Read default Ethernet interface configuration from resources
+        final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
+        for (String strConfig : interfaceConfigs) {
+            parseEthernetConfig(strConfig);
+        }
+
+        mConfigStore = new EthernetConfigStore();
+    }
+
+    void start() {
+        mFactory.register();
+        mConfigStore.read();
+
+        // Default interface is just the first one we want to track.
+        mIpConfigForDefaultInterface = mConfigStore.getIpConfigurationForDefaultInterface();
+        final ArrayMap<String, IpConfiguration> configs = mConfigStore.getIpConfigurations();
+        for (int i = 0; i < configs.size(); i++) {
+            mIpConfigurations.put(configs.keyAt(i), configs.valueAt(i));
+        }
+
+        try {
+            PermissionUtils.enforceNetworkStackPermission(mContext);
+            mNetd.registerUnsolicitedEventListener(new InterfaceObserver());
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Could not register InterfaceObserver " + e);
+        }
+
+        mHandler.post(this::trackAvailableInterfaces);
+    }
+
+    void updateIpConfiguration(String iface, IpConfiguration ipConfiguration) {
+        if (DBG) {
+            Log.i(TAG, "updateIpConfiguration, iface: " + iface + ", cfg: " + ipConfiguration);
+        }
+        writeIpConfiguration(iface, ipConfiguration);
+        mHandler.post(() -> {
+            mFactory.updateInterface(iface, ipConfiguration, null, null);
+            broadcastInterfaceStateChange(iface);
+        });
+    }
+
+    private void writeIpConfiguration(@NonNull final String iface,
+            @NonNull final IpConfiguration ipConfig) {
+        mConfigStore.write(iface, ipConfig);
+        mIpConfigurations.put(iface, ipConfig);
+    }
+
+    private IpConfiguration getIpConfigurationForCallback(String iface, int state) {
+        return (state == EthernetManager.STATE_ABSENT) ? null : getOrCreateIpConfiguration(iface);
+    }
+
+    private void ensureRunningOnEthernetServiceThread() {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on EthernetService thread: "
+                            + Thread.currentThread().getName());
+        }
+    }
+
+    /**
+     * Broadcast the link state or IpConfiguration change of existing Ethernet interfaces to all
+     * listeners.
+     */
+    protected void broadcastInterfaceStateChange(@NonNull String iface) {
+        ensureRunningOnEthernetServiceThread();
+        final int state = mFactory.getInterfaceState(iface);
+        final int role = getInterfaceRole(iface);
+        final IpConfiguration config = getIpConfigurationForCallback(iface, state);
+        final int n = mListeners.beginBroadcast();
+        for (int i = 0; i < n; i++) {
+            try {
+                mListeners.getBroadcastItem(i).onInterfaceStateChanged(iface, state, role, config);
+            } catch (RemoteException e) {
+                // Do nothing here.
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+
+    /**
+     * Unicast the interface state or IpConfiguration change of existing Ethernet interfaces to a
+     * specific listener.
+     */
+    protected void unicastInterfaceStateChange(@NonNull IEthernetServiceListener listener,
+            @NonNull String iface) {
+        ensureRunningOnEthernetServiceThread();
+        final int state = mFactory.getInterfaceState(iface);
+        final int role = getInterfaceRole(iface);
+        final IpConfiguration config = getIpConfigurationForCallback(iface, state);
+        try {
+            listener.onInterfaceStateChanged(iface, state, role, config);
+        } catch (RemoteException e) {
+            // Do nothing here.
+        }
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void updateConfiguration(@NonNull final String iface,
+            @Nullable final IpConfiguration ipConfig,
+            @Nullable final NetworkCapabilities capabilities,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (DBG) {
+            Log.i(TAG, "updateConfiguration, iface: " + iface + ", capabilities: " + capabilities
+                    + ", ipConfig: " + ipConfig);
+        }
+
+        final IpConfiguration localIpConfig = ipConfig == null
+                ? null : new IpConfiguration(ipConfig);
+        if (ipConfig != null) {
+            writeIpConfiguration(iface, localIpConfig);
+        }
+
+        if (null != capabilities) {
+            mNetworkCapabilities.put(iface, capabilities);
+        }
+        mHandler.post(() -> {
+            mFactory.updateInterface(iface, localIpConfig, capabilities, listener);
+            broadcastInterfaceStateChange(iface);
+        });
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void connectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        mHandler.post(() -> updateInterfaceState(iface, true, listener));
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void disconnectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        mHandler.post(() -> updateInterfaceState(iface, false, listener));
+    }
+
+    IpConfiguration getIpConfiguration(String iface) {
+        return mIpConfigurations.get(iface);
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected boolean isTrackingInterface(String iface) {
+        return mFactory.hasInterface(iface);
+    }
+
+    String[] getInterfaces(boolean includeRestricted) {
+        return mFactory.getAvailableInterfaces(includeRestricted);
+    }
+
+    List<String> getInterfaceList() {
+        final List<String> interfaceList = new ArrayList<String>();
+        final String[] ifaces;
+        try {
+            ifaces = mNetd.interfaceGetList();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not get list of interfaces " + e);
+            return interfaceList;
+        }
+        final String ifaceMatch = mIfaceMatch;
+        for (String iface : ifaces) {
+            if (iface.matches(ifaceMatch)) interfaceList.add(iface);
+        }
+        return interfaceList;
+    }
+
+    /**
+     * Returns true if given interface was configured as restricted (doesn't have
+     * NET_CAPABILITY_NOT_RESTRICTED) capability. Otherwise, returns false.
+     */
+    boolean isRestrictedInterface(String iface) {
+        final NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+        return nc != null && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+    }
+
+    void addListener(IEthernetServiceListener listener, boolean canUseRestrictedNetworks) {
+        mHandler.post(() -> {
+            if (!mListeners.register(listener, new ListenerInfo(canUseRestrictedNetworks))) {
+                // Remote process has already died
+                return;
+            }
+            for (String iface : getInterfaces(canUseRestrictedNetworks)) {
+                unicastInterfaceStateChange(listener, iface);
+            }
+
+            unicastEthernetStateChange(listener, mEthernetState);
+        });
+    }
+
+    void removeListener(IEthernetServiceListener listener) {
+        mHandler.post(() -> mListeners.unregister(listener));
+    }
+
+    public void setIncludeTestInterfaces(boolean include) {
+        mHandler.post(() -> {
+            mIncludeTestInterfaces = include;
+            updateIfaceMatchRegexp();
+            if (!include) {
+                removeTestData();
+            }
+            mHandler.post(() -> trackAvailableInterfaces());
+        });
+    }
+
+    private void removeTestData() {
+        removeTestIpData();
+        removeTestCapabilityData();
+    }
+
+    private void removeTestIpData() {
+        final Iterator<String> iterator = mIpConfigurations.keySet().iterator();
+        while (iterator.hasNext()) {
+            final String iface = iterator.next();
+            if (iface.matches(TEST_IFACE_REGEXP)) {
+                mConfigStore.write(iface, null);
+                iterator.remove();
+            }
+        }
+    }
+
+    private void removeTestCapabilityData() {
+        mNetworkCapabilities.keySet().removeIf(iface -> iface.matches(TEST_IFACE_REGEXP));
+    }
+
+    public void requestTetheredInterface(ITetheredInterfaceCallback callback) {
+        mHandler.post(() -> {
+            if (!mTetheredInterfaceRequests.register(callback)) {
+                // Remote process has already died
+                return;
+            }
+            if (mDefaultInterfaceMode == INTERFACE_MODE_SERVER) {
+                if (mTetheredInterfaceWasAvailable) {
+                    notifyTetheredInterfaceAvailable(callback, mDefaultInterface);
+                }
+                return;
+            }
+
+            setDefaultInterfaceMode(INTERFACE_MODE_SERVER);
+        });
+    }
+
+    public void releaseTetheredInterface(ITetheredInterfaceCallback callback) {
+        mHandler.post(() -> {
+            mTetheredInterfaceRequests.unregister(callback);
+            maybeUntetherDefaultInterface();
+        });
+    }
+
+    private void notifyTetheredInterfaceAvailable(ITetheredInterfaceCallback cb, String iface) {
+        try {
+            cb.onAvailable(iface);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending tethered interface available callback", e);
+        }
+    }
+
+    private void notifyTetheredInterfaceUnavailable(ITetheredInterfaceCallback cb) {
+        try {
+            cb.onUnavailable();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending tethered interface available callback", e);
+        }
+    }
+
+    private void maybeUntetherDefaultInterface() {
+        if (mTetheredInterfaceRequests.getRegisteredCallbackCount() > 0) return;
+        if (mDefaultInterfaceMode == INTERFACE_MODE_CLIENT) return;
+        setDefaultInterfaceMode(INTERFACE_MODE_CLIENT);
+    }
+
+    private void setDefaultInterfaceMode(int mode) {
+        Log.d(TAG, "Setting default interface mode to " + mode);
+        mDefaultInterfaceMode = mode;
+        if (mDefaultInterface != null) {
+            removeInterface(mDefaultInterface);
+            addInterface(mDefaultInterface);
+        }
+    }
+
+    private int getInterfaceRole(final String iface) {
+        if (!mFactory.hasInterface(iface)) return EthernetManager.ROLE_NONE;
+        final int mode = getInterfaceMode(iface);
+        return (mode == INTERFACE_MODE_CLIENT)
+                ? EthernetManager.ROLE_CLIENT
+                : EthernetManager.ROLE_SERVER;
+    }
+
+    private int getInterfaceMode(final String iface) {
+        if (iface.equals(mDefaultInterface)) {
+            return mDefaultInterfaceMode;
+        }
+        return INTERFACE_MODE_CLIENT;
+    }
+
+    private void removeInterface(String iface) {
+        mFactory.removeInterface(iface);
+        maybeUpdateServerModeInterfaceState(iface, false);
+    }
+
+    private void stopTrackingInterface(String iface) {
+        removeInterface(iface);
+        if (iface.equals(mDefaultInterface)) {
+            mDefaultInterface = null;
+        }
+        broadcastInterfaceStateChange(iface);
+    }
+
+    private void addInterface(String iface) {
+        InterfaceConfigurationParcel config = null;
+        // Bring up the interface so we get link status indications.
+        try {
+            PermissionUtils.enforceNetworkStackPermission(mContext);
+            NetdUtils.setInterfaceUp(mNetd, iface);
+            config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
+        } catch (IllegalStateException e) {
+            // Either the system is crashing or the interface has disappeared. Just ignore the
+            // error; we haven't modified any state because we only do that if our calls succeed.
+            Log.e(TAG, "Error upping interface " + iface, e);
+        }
+
+        if (config == null) {
+            Log.e(TAG, "Null interface config parcelable for " + iface + ". Bailing out.");
+            return;
+        }
+
+        final String hwAddress = config.hwAddr;
+
+        NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+        if (nc == null) {
+            // Try to resolve using mac address
+            nc = mNetworkCapabilities.get(hwAddress);
+            if (nc == null) {
+                final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
+                nc = createDefaultNetworkCapabilities(isTestIface);
+            }
+        }
+
+        final int mode = getInterfaceMode(iface);
+        if (mode == INTERFACE_MODE_CLIENT) {
+            IpConfiguration ipConfiguration = getOrCreateIpConfiguration(iface);
+            Log.d(TAG, "Tracking interface in client mode: " + iface);
+            mFactory.addInterface(iface, hwAddress, ipConfiguration, nc);
+        } else {
+            maybeUpdateServerModeInterfaceState(iface, true);
+        }
+
+        // Note: if the interface already has link (e.g., if we crashed and got
+        // restarted while it was running), we need to fake a link up notification so we
+        // start configuring it.
+        if (NetdUtils.hasFlag(config, "running")) {
+            updateInterfaceState(iface, true);
+        }
+    }
+
+    private void updateInterfaceState(String iface, boolean up) {
+        updateInterfaceState(iface, up, null /* listener */);
+    }
+
+    private void updateInterfaceState(@NonNull final String iface, final boolean up,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        final int mode = getInterfaceMode(iface);
+        final boolean factoryLinkStateUpdated = (mode == INTERFACE_MODE_CLIENT)
+                && mFactory.updateInterfaceLinkState(iface, up, listener);
+
+        if (factoryLinkStateUpdated) {
+            broadcastInterfaceStateChange(iface);
+        }
+    }
+
+    private void maybeUpdateServerModeInterfaceState(String iface, boolean available) {
+        if (available == mTetheredInterfaceWasAvailable || !iface.equals(mDefaultInterface)) return;
+
+        Log.d(TAG, (available ? "Tracking" : "No longer tracking")
+                + " interface in server mode: " + iface);
+
+        final int pendingCbs = mTetheredInterfaceRequests.beginBroadcast();
+        for (int i = 0; i < pendingCbs; i++) {
+            ITetheredInterfaceCallback item = mTetheredInterfaceRequests.getBroadcastItem(i);
+            if (available) {
+                notifyTetheredInterfaceAvailable(item, iface);
+            } else {
+                notifyTetheredInterfaceUnavailable(item);
+            }
+        }
+        mTetheredInterfaceRequests.finishBroadcast();
+        mTetheredInterfaceWasAvailable = available;
+    }
+
+    private void maybeTrackInterface(String iface) {
+        if (!iface.matches(mIfaceMatch)) {
+            return;
+        }
+
+        // If we don't already track this interface, and if this interface matches
+        // our regex, start tracking it.
+        if (mFactory.hasInterface(iface) || iface.equals(mDefaultInterface)) {
+            if (DBG) Log.w(TAG, "Ignoring already-tracked interface " + iface);
+            return;
+        }
+        if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface);
+
+        // TODO: avoid making an interface default if it has configured NetworkCapabilities.
+        if (mDefaultInterface == null) {
+            mDefaultInterface = iface;
+        }
+
+        if (mIpConfigForDefaultInterface != null) {
+            updateIpConfiguration(iface, mIpConfigForDefaultInterface);
+            mIpConfigForDefaultInterface = null;
+        }
+
+        addInterface(iface);
+
+        broadcastInterfaceStateChange(iface);
+    }
+
+    private void trackAvailableInterfaces() {
+        try {
+            final String[] ifaces = mNetd.interfaceGetList();
+            for (String iface : ifaces) {
+                maybeTrackInterface(iface);
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Could not get list of interfaces " + e);
+        }
+    }
+
+    private class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
+
+        @Override
+        public void onInterfaceLinkStateChanged(String iface, boolean up) {
+            if (DBG) {
+                Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up);
+            }
+            mHandler.post(() -> updateInterfaceState(iface, up));
+        }
+
+        @Override
+        public void onInterfaceAdded(String iface) {
+            if (DBG) {
+                Log.i(TAG, "onInterfaceAdded, iface: " + iface);
+            }
+            mHandler.post(() -> maybeTrackInterface(iface));
+        }
+
+        @Override
+        public void onInterfaceRemoved(String iface) {
+            if (DBG) {
+                Log.i(TAG, "onInterfaceRemoved, iface: " + iface);
+            }
+            mHandler.post(() -> stopTrackingInterface(iface));
+        }
+    }
+
+    private static class ListenerInfo {
+
+        boolean canUseRestrictedNetworks = false;
+
+        ListenerInfo(boolean canUseRestrictedNetworks) {
+            this.canUseRestrictedNetworks = canUseRestrictedNetworks;
+        }
+    }
+
+    /**
+     * Parses an Ethernet interface configuration
+     *
+     * @param configString represents an Ethernet configuration in the following format: {@code
+     * <interface name|mac address>;[Network Capabilities];[IP config];[Override Transport]}
+     */
+    private void parseEthernetConfig(String configString) {
+        final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
+        NetworkCapabilities nc = createNetworkCapabilities(
+                !TextUtils.isEmpty(config.mCapabilities)  /* clear default capabilities */,
+                config.mCapabilities, config.mTransport).build();
+        mNetworkCapabilities.put(config.mIface, nc);
+
+        if (null != config.mIpConfig) {
+            IpConfiguration ipConfig = parseStaticIpConfiguration(config.mIpConfig);
+            mIpConfigurations.put(config.mIface, ipConfig);
+        }
+    }
+
+    @VisibleForTesting
+    static EthernetTrackerConfig createEthernetTrackerConfig(@NonNull final String configString) {
+        Objects.requireNonNull(configString, "EthernetTrackerConfig requires non-null config");
+        return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
+        NetworkCapabilities.Builder builder = createNetworkCapabilities(
+                false /* clear default capabilities */, null, null)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+
+        if (isTestIface) {
+            builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
+        } else {
+            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * Parses a static list of network capabilities
+     *
+     * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities
+     * @param commaSeparatedCapabilities A comma separated string list of integer encoded
+     *                                   NetworkCapability.NET_CAPABILITY_* values
+     * @param overrideTransport A string representing a single integer encoded override transport
+     *                          type. Must be one of the NetworkCapability.TRANSPORT_*
+     *                          values. TRANSPORT_VPN is not supported. Errors with input
+     *                          will cause the override to be ignored.
+     */
+    @VisibleForTesting
+    static NetworkCapabilities.Builder createNetworkCapabilities(
+            boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities,
+            @Nullable String overrideTransport) {
+
+        final NetworkCapabilities.Builder builder = clearDefaultCapabilities
+                ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                : new NetworkCapabilities.Builder();
+
+        // Determine the transport type. If someone has tried to define an override transport then
+        // attempt to add it. Since we can only have one override, all errors with it will
+        // gracefully default back to TRANSPORT_ETHERNET and warn the user. VPN is not allowed as an
+        // override type. Wifi Aware and LoWPAN are currently unsupported as well.
+        int transport = NetworkCapabilities.TRANSPORT_ETHERNET;
+        if (!TextUtils.isEmpty(overrideTransport)) {
+            try {
+                int parsedTransport = Integer.valueOf(overrideTransport);
+                if (parsedTransport == NetworkCapabilities.TRANSPORT_VPN
+                        || parsedTransport == NetworkCapabilities.TRANSPORT_WIFI_AWARE
+                        || parsedTransport == NetworkCapabilities.TRANSPORT_LOWPAN) {
+                    Log.e(TAG, "Override transport '" + parsedTransport + "' is not supported. "
+                            + "Defaulting to TRANSPORT_ETHERNET");
+                } else {
+                    transport = parsedTransport;
+                }
+            } catch (NumberFormatException nfe) {
+                Log.e(TAG, "Override transport type '" + overrideTransport + "' "
+                        + "could not be parsed. Defaulting to TRANSPORT_ETHERNET");
+            }
+        }
+
+        // Apply the transport. If the user supplied a valid number that is not a valid transport
+        // then adding will throw an exception. Default back to TRANSPORT_ETHERNET if that happens
+        try {
+            builder.addTransportType(transport);
+        } catch (IllegalArgumentException iae) {
+            Log.e(TAG, transport + " is not a valid NetworkCapability.TRANSPORT_* value. "
+                    + "Defaulting to TRANSPORT_ETHERNET");
+            builder.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET);
+        }
+
+        builder.setLinkUpstreamBandwidthKbps(100 * 1000);
+        builder.setLinkDownstreamBandwidthKbps(100 * 1000);
+
+        if (!TextUtils.isEmpty(commaSeparatedCapabilities)) {
+            for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) {
+                if (!TextUtils.isEmpty(strNetworkCapability)) {
+                    try {
+                        builder.addCapability(Integer.valueOf(strNetworkCapability));
+                    } catch (NumberFormatException nfe) {
+                        Log.e(TAG, "Capability '" + strNetworkCapability + "' could not be parsed");
+                    } catch (IllegalArgumentException iae) {
+                        Log.e(TAG, strNetworkCapability + " is not a valid "
+                                + "NetworkCapability.NET_CAPABILITY_* value");
+                    }
+                }
+            }
+        }
+        // Ethernet networks have no way to update the following capabilities, so they always
+        // have them.
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+
+        return builder;
+    }
+
+    /**
+     * Parses static IP configuration.
+     *
+     * @param staticIpConfig represents static IP configuration in the following format: {@code
+     * ip=<ip-address/mask> gateway=<ip-address> dns=<comma-sep-ip-addresses>
+     *     domains=<comma-sep-domains>}
+     */
+    @VisibleForTesting
+    static IpConfiguration parseStaticIpConfiguration(String staticIpConfig) {
+        final StaticIpConfiguration.Builder staticIpConfigBuilder =
+                new StaticIpConfiguration.Builder();
+
+        for (String keyValueAsString : staticIpConfig.trim().split(" ")) {
+            if (TextUtils.isEmpty(keyValueAsString)) continue;
+
+            String[] pair = keyValueAsString.split("=");
+            if (pair.length != 2) {
+                throw new IllegalArgumentException("Unexpected token: " + keyValueAsString
+                        + " in " + staticIpConfig);
+            }
+
+            String key = pair[0];
+            String value = pair[1];
+
+            switch (key) {
+                case "ip":
+                    staticIpConfigBuilder.setIpAddress(new LinkAddress(value));
+                    break;
+                case "domains":
+                    staticIpConfigBuilder.setDomains(value);
+                    break;
+                case "gateway":
+                    staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));
+                    break;
+                case "dns": {
+                    ArrayList<InetAddress> dnsAddresses = new ArrayList<>();
+                    for (String address: value.split(",")) {
+                        dnsAddresses.add(InetAddress.parseNumericAddress(address));
+                    }
+                    staticIpConfigBuilder.setDnsServers(dnsAddresses);
+                    break;
+                }
+                default : {
+                    throw new IllegalArgumentException("Unexpected key: " + key
+                            + " in " + staticIpConfig);
+                }
+            }
+        }
+        return createIpConfiguration(staticIpConfigBuilder.build());
+    }
+
+    private static IpConfiguration createIpConfiguration(
+            @NonNull final StaticIpConfiguration staticIpConfig) {
+        return new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
+    }
+
+    private IpConfiguration getOrCreateIpConfiguration(String iface) {
+        IpConfiguration ret = mIpConfigurations.get(iface);
+        if (ret != null) return ret;
+        ret = new IpConfiguration();
+        ret.setIpAssignment(IpAssignment.DHCP);
+        ret.setProxySettings(ProxySettings.NONE);
+        return ret;
+    }
+
+    private void updateIfaceMatchRegexp() {
+        final String match = mDeps.getInterfaceRegexFromResource(mContext);
+        mIfaceMatch = mIncludeTestInterfaces
+                ? "(" + match + "|" + TEST_IFACE_REGEXP + ")"
+                : match;
+        Log.d(TAG, "Interface match regexp set to '" + mIfaceMatch + "'");
+    }
+
+    /**
+     * Validate if a given interface is valid for testing.
+     *
+     * @param iface the name of the interface to validate.
+     * @return {@code true} if test interfaces are enabled and the given {@code iface} has a test
+     * interface prefix, {@code false} otherwise.
+     */
+    public boolean isValidTestInterface(@NonNull final String iface) {
+        return mIncludeTestInterfaces && iface.matches(TEST_IFACE_REGEXP);
+    }
+
+    private void postAndWaitForRunnable(Runnable r) {
+        final ConditionVariable cv = new ConditionVariable();
+        if (mHandler.post(() -> {
+            r.run();
+            cv.open();
+        })) {
+            cv.block(2000L);
+        }
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void setEthernetEnabled(boolean enabled) {
+        mHandler.post(() -> {
+            int newState = enabled ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED;
+            if (mEthernetState == newState) return;
+
+            mEthernetState = newState;
+
+            if (enabled) {
+                trackAvailableInterfaces();
+            } else {
+                // TODO: maybe also disable server mode interface as well.
+                untrackFactoryInterfaces();
+            }
+            broadcastEthernetStateChange(mEthernetState);
+        });
+    }
+
+    private void untrackFactoryInterfaces() {
+        for (String iface : mFactory.getAvailableInterfaces(true /* includeRestricted */)) {
+            stopTrackingInterface(iface);
+        }
+    }
+
+    private void unicastEthernetStateChange(@NonNull IEthernetServiceListener listener,
+            int state) {
+        ensureRunningOnEthernetServiceThread();
+        try {
+            listener.onEthernetStateChanged(state);
+        } catch (RemoteException e) {
+            // Do nothing here.
+        }
+    }
+
+    private void broadcastEthernetStateChange(int state) {
+        ensureRunningOnEthernetServiceThread();
+        final int n = mListeners.beginBroadcast();
+        for (int i = 0; i < n; i++) {
+            try {
+                mListeners.getBroadcastItem(i).onEthernetStateChanged(state);
+            } catch (RemoteException e) {
+                // Do nothing here.
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
+        postAndWaitForRunnable(() -> {
+            pw.println(getClass().getSimpleName());
+            pw.println("Ethernet interface name filter: " + mIfaceMatch);
+            pw.println("Default interface: " + mDefaultInterface);
+            pw.println("Default interface mode: " + mDefaultInterfaceMode);
+            pw.println("Tethered interface requests: "
+                    + mTetheredInterfaceRequests.getRegisteredCallbackCount());
+            pw.println("Listeners: " + mListeners.getRegisteredCallbackCount());
+            pw.println("IP Configurations:");
+            pw.increaseIndent();
+            for (String iface : mIpConfigurations.keySet()) {
+                pw.println(iface + ": " + mIpConfigurations.get(iface));
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            pw.println("Network Capabilities:");
+            pw.increaseIndent();
+            for (String iface : mNetworkCapabilities.keySet()) {
+                pw.println(iface + ": " + mNetworkCapabilities.get(iface));
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            mFactory.dump(fd, pw, args);
+        });
+    }
+
+    @VisibleForTesting
+    static class EthernetTrackerConfig {
+        final String mIface;
+        final String mCapabilities;
+        final String mIpConfig;
+        final String mTransport;
+
+        EthernetTrackerConfig(@NonNull final String[] tokens) {
+            Objects.requireNonNull(tokens, "EthernetTrackerConfig requires non-null tokens");
+            mIface = tokens[0];
+            mCapabilities = tokens.length > 1 ? tokens[1] : null;
+            mIpConfig = tokens.length > 2 && !TextUtils.isEmpty(tokens[2]) ? tokens[2] : null;
+            mTransport = tokens.length > 3 ? tokens[3] : null;
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
new file mode 100644
index 0000000..25c88eb
--- /dev/null
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net;
+
+import android.content.Context;
+import android.net.INetd;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.Struct.U32;
+
+/**
+ * Monitor interface added (without removed) and right interface name and its index to bpf map.
+ */
+public class BpfInterfaceMapUpdater {
+    private static final String TAG = BpfInterfaceMapUpdater.class.getSimpleName();
+    // This is current path but may be changed soon.
+    private static final String IFACE_INDEX_NAME_MAP_PATH =
+            "/sys/fs/bpf/map_netd_iface_index_name_map";
+    private final IBpfMap<U32, InterfaceMapValue> mBpfMap;
+    private final INetd mNetd;
+    private final Handler mHandler;
+    private final Dependencies mDeps;
+
+    public BpfInterfaceMapUpdater(Context ctx, Handler handler) {
+        this(ctx, handler, new Dependencies());
+    }
+
+    @VisibleForTesting
+    public BpfInterfaceMapUpdater(Context ctx, Handler handler, Dependencies deps) {
+        mDeps = deps;
+        mBpfMap = deps.getInterfaceMap();
+        mNetd = deps.getINetd(ctx);
+        mHandler = handler;
+    }
+
+    /**
+     * Dependencies of BpfInerfaceMapUpdater, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Create BpfMap for updating interface and index mapping. */
+        public IBpfMap<U32, InterfaceMapValue> getInterfaceMap() {
+            try {
+                return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH, BpfMap.BPF_F_RDWR,
+                    U32.class, InterfaceMapValue.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create interface map: " + e);
+                return null;
+            }
+        }
+
+        /** Get InterfaceParams for giving interface name. */
+        public InterfaceParams getInterfaceParams(String ifaceName) {
+            return InterfaceParams.getByName(ifaceName);
+        }
+
+        /** Get INetd binder object. */
+        public INetd getINetd(Context ctx) {
+            return INetd.Stub.asInterface((IBinder) ctx.getSystemService(Context.NETD_SERVICE));
+        }
+    }
+
+    /**
+     * Start listening interface update event.
+     * Query current interface names before listening.
+     */
+    public void start() {
+        mHandler.post(() -> {
+            if (mBpfMap == null) {
+                Log.wtf(TAG, "Fail to start: Null bpf map");
+                return;
+            }
+
+            try {
+                // TODO: use a NetlinkMonitor and listen for RTM_NEWLINK messages instead.
+                mNetd.registerUnsolicitedEventListener(new InterfaceChangeObserver());
+            } catch (RemoteException e) {
+                Log.wtf(TAG, "Unable to register netd UnsolicitedEventListener, " + e);
+            }
+
+            final String[] ifaces;
+            try {
+                // TODO: use a netlink dump to get the current interface list.
+                ifaces = mNetd.interfaceGetList();
+            } catch (RemoteException | ServiceSpecificException e) {
+                Log.wtf(TAG, "Unable to query interface names by netd, " + e);
+                return;
+            }
+
+            for (String ifaceName : ifaces) {
+                addInterface(ifaceName);
+            }
+        });
+    }
+
+    private void addInterface(String ifaceName) {
+        final InterfaceParams iface = mDeps.getInterfaceParams(ifaceName);
+        if (iface == null) {
+            Log.e(TAG, "Unable to get InterfaceParams for " + ifaceName);
+            return;
+        }
+
+        try {
+            mBpfMap.updateEntry(new U32(iface.index), new InterfaceMapValue(ifaceName));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Unable to update entry for " + ifaceName + ", " + e);
+        }
+    }
+
+    private class InterfaceChangeObserver extends BaseNetdUnsolicitedEventListener {
+        @Override
+        public void onInterfaceAdded(String ifName) {
+            mHandler.post(() -> addInterface(ifName));
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/net/CookieTagMapKey.java b/service-t/src/com/android/server/net/CookieTagMapKey.java
new file mode 100644
index 0000000..443e5b3
--- /dev/null
+++ b/service-t/src/com/android/server/net/CookieTagMapKey.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * Key for cookie tag map.
+ */
+public class CookieTagMapKey extends Struct {
+    @Field(order = 0, type = Type.S64)
+    public final long socketCookie;
+
+    public CookieTagMapKey(final long socketCookie) {
+        this.socketCookie = socketCookie;
+    }
+}
diff --git a/service-t/src/com/android/server/net/CookieTagMapValue.java b/service-t/src/com/android/server/net/CookieTagMapValue.java
new file mode 100644
index 0000000..93b9195
--- /dev/null
+++ b/service-t/src/com/android/server/net/CookieTagMapValue.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * Value for cookie tag map.
+ */
+public class CookieTagMapValue extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long uid;
+
+    @Field(order = 1, type = Type.U32)
+    public final long tag;
+
+    public CookieTagMapValue(final long uid, final long tag) {
+        this.uid = uid;
+        this.tag = tag;
+    }
+}
diff --git a/service-t/src/com/android/server/net/InterfaceMapValue.java b/service-t/src/com/android/server/net/InterfaceMapValue.java
new file mode 100644
index 0000000..42c0044
--- /dev/null
+++ b/service-t/src/com/android/server/net/InterfaceMapValue.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * The value of bpf interface index map which is used for NetworkStatsService.
+ */
+public class InterfaceMapValue extends Struct {
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] interfaceName;
+
+    public InterfaceMapValue(String iface) {
+        final byte[] ifaceArray = iface.getBytes();
+        interfaceName = new byte[16];
+        // All array bytes after the interface name, if any, must be 0.
+        System.arraycopy(ifaceArray, 0, interfaceName, 0, ifaceArray.length);
+    }
+}
diff --git a/service-t/src/com/android/server/net/IpConfigStore.java b/service-t/src/com/android/server/net/IpConfigStore.java
new file mode 100644
index 0000000..3a9a544
--- /dev/null
+++ b/service-t/src/com/android/server/net/IpConfigStore.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.net.InetAddresses;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+import android.net.Uri;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.ProxyUtils;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class provides an API to store and manage L3 network IP configuration.
+ */
+public class IpConfigStore {
+    private static final String TAG = "IpConfigStore";
+    private static final boolean DBG = false;
+
+    protected final DelayedDiskWrite mWriter;
+
+    /* IP and proxy configuration keys */
+    protected static final String ID_KEY = "id";
+    protected static final String IP_ASSIGNMENT_KEY = "ipAssignment";
+    protected static final String LINK_ADDRESS_KEY = "linkAddress";
+    protected static final String GATEWAY_KEY = "gateway";
+    protected static final String DNS_KEY = "dns";
+    protected static final String PROXY_SETTINGS_KEY = "proxySettings";
+    protected static final String PROXY_HOST_KEY = "proxyHost";
+    protected static final String PROXY_PORT_KEY = "proxyPort";
+    protected static final String PROXY_PAC_FILE = "proxyPac";
+    protected static final String EXCLUSION_LIST_KEY = "exclusionList";
+    protected static final String EOS = "eos";
+
+    protected static final int IPCONFIG_FILE_VERSION = 3;
+
+    public IpConfigStore(DelayedDiskWrite writer) {
+        mWriter = writer;
+    }
+
+    public IpConfigStore() {
+        this(new DelayedDiskWrite());
+    }
+
+    private static boolean writeConfig(DataOutputStream out, String configKey,
+            IpConfiguration config) throws IOException {
+        return writeConfig(out, configKey, config, IPCONFIG_FILE_VERSION);
+    }
+
+    /**
+     *  Write the IP configuration with the given parameters to {@link DataOutputStream}.
+     */
+    @VisibleForTesting
+    public static boolean writeConfig(DataOutputStream out, String configKey,
+                                IpConfiguration config, int version) throws IOException {
+        boolean written = false;
+
+        try {
+            switch (config.getIpAssignment()) {
+                case STATIC:
+                    out.writeUTF(IP_ASSIGNMENT_KEY);
+                    out.writeUTF(config.getIpAssignment().toString());
+                    StaticIpConfiguration staticIpConfiguration = config.getStaticIpConfiguration();
+                    if (staticIpConfiguration != null) {
+                        if (staticIpConfiguration.getIpAddress() != null) {
+                            LinkAddress ipAddress = staticIpConfiguration.getIpAddress();
+                            out.writeUTF(LINK_ADDRESS_KEY);
+                            out.writeUTF(ipAddress.getAddress().getHostAddress());
+                            out.writeInt(ipAddress.getPrefixLength());
+                        }
+                        if (staticIpConfiguration.getGateway() != null) {
+                            out.writeUTF(GATEWAY_KEY);
+                            out.writeInt(0);  // Default route.
+                            out.writeInt(1);  // Have a gateway.
+                            out.writeUTF(staticIpConfiguration.getGateway().getHostAddress());
+                        }
+                        for (InetAddress inetAddr : staticIpConfiguration.getDnsServers()) {
+                            out.writeUTF(DNS_KEY);
+                            out.writeUTF(inetAddr.getHostAddress());
+                        }
+                    }
+                    written = true;
+                    break;
+                case DHCP:
+                    out.writeUTF(IP_ASSIGNMENT_KEY);
+                    out.writeUTF(config.getIpAssignment().toString());
+                    written = true;
+                    break;
+                case UNASSIGNED:
+                /* Ignore */
+                    break;
+                default:
+                    loge("Ignore invalid ip assignment while writing");
+                    break;
+            }
+
+            switch (config.getProxySettings()) {
+                case STATIC:
+                    ProxyInfo proxyProperties = config.getHttpProxy();
+                    String exclusionList = ProxyUtils.exclusionListAsString(
+                            proxyProperties.getExclusionList());
+                    out.writeUTF(PROXY_SETTINGS_KEY);
+                    out.writeUTF(config.getProxySettings().toString());
+                    out.writeUTF(PROXY_HOST_KEY);
+                    out.writeUTF(proxyProperties.getHost());
+                    out.writeUTF(PROXY_PORT_KEY);
+                    out.writeInt(proxyProperties.getPort());
+                    if (exclusionList != null) {
+                        out.writeUTF(EXCLUSION_LIST_KEY);
+                        out.writeUTF(exclusionList);
+                    }
+                    written = true;
+                    break;
+                case PAC:
+                    ProxyInfo proxyPacProperties = config.getHttpProxy();
+                    out.writeUTF(PROXY_SETTINGS_KEY);
+                    out.writeUTF(config.getProxySettings().toString());
+                    out.writeUTF(PROXY_PAC_FILE);
+                    out.writeUTF(proxyPacProperties.getPacFileUrl().toString());
+                    written = true;
+                    break;
+                case NONE:
+                    out.writeUTF(PROXY_SETTINGS_KEY);
+                    out.writeUTF(config.getProxySettings().toString());
+                    written = true;
+                    break;
+                case UNASSIGNED:
+                    /* Ignore */
+                    break;
+                default:
+                    loge("Ignore invalid proxy settings while writing");
+                    break;
+            }
+
+            if (written) {
+                out.writeUTF(ID_KEY);
+                if (version < 3) {
+                    out.writeInt(Integer.valueOf(configKey));
+                } else {
+                    out.writeUTF(configKey);
+                }
+            }
+        } catch (NullPointerException e) {
+            loge("Failure in writing " + config + e);
+        }
+        out.writeUTF(EOS);
+
+        return written;
+    }
+
+    /**
+     * @deprecated use {@link #writeIpConfigurations(String, ArrayMap)} instead.
+     * New method uses string as network identifier which could be interface name or MAC address or
+     * other token.
+     */
+    @Deprecated
+    public void writeIpAndProxyConfigurationsToFile(String filePath,
+                                              final SparseArray<IpConfiguration> networks) {
+        mWriter.write(filePath, out -> {
+            out.writeInt(IPCONFIG_FILE_VERSION);
+            for (int i = 0; i < networks.size(); i++) {
+                writeConfig(out, String.valueOf(networks.keyAt(i)), networks.valueAt(i));
+            }
+        });
+    }
+
+    /**
+     *  Write the IP configuration associated to the target networks to the destination path.
+     */
+    public void writeIpConfigurations(String filePath,
+                                      ArrayMap<String, IpConfiguration> networks) {
+        mWriter.write(filePath, out -> {
+            out.writeInt(IPCONFIG_FILE_VERSION);
+            for (int i = 0; i < networks.size(); i++) {
+                writeConfig(out, networks.keyAt(i), networks.valueAt(i));
+            }
+        });
+    }
+
+    /**
+     * Read the IP configuration from the destination path to {@link BufferedInputStream}.
+     */
+    public static ArrayMap<String, IpConfiguration> readIpConfigurations(String filePath) {
+        BufferedInputStream bufferedInputStream;
+        try {
+            bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
+        } catch (FileNotFoundException e) {
+            // Return an empty array here because callers expect an empty array when the file is
+            // not present.
+            loge("Error opening configuration file: " + e);
+            return new ArrayMap<>(0);
+        }
+        return readIpConfigurations(bufferedInputStream);
+    }
+
+    /** @deprecated use {@link #readIpConfigurations(String)} */
+    @Deprecated
+    public static SparseArray<IpConfiguration> readIpAndProxyConfigurations(String filePath) {
+        BufferedInputStream bufferedInputStream;
+        try {
+            bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
+        } catch (FileNotFoundException e) {
+            // Return an empty array here because callers expect an empty array when the file is
+            // not present.
+            loge("Error opening configuration file: " + e);
+            return new SparseArray<>();
+        }
+        return readIpAndProxyConfigurations(bufferedInputStream);
+    }
+
+    /** @deprecated use {@link #readIpConfigurations(InputStream)} */
+    @Deprecated
+    public static SparseArray<IpConfiguration> readIpAndProxyConfigurations(
+            InputStream inputStream) {
+        ArrayMap<String, IpConfiguration> networks = readIpConfigurations(inputStream);
+        if (networks == null) {
+            return null;
+        }
+
+        SparseArray<IpConfiguration> networksById = new SparseArray<>();
+        for (int i = 0; i < networks.size(); i++) {
+            int id = Integer.valueOf(networks.keyAt(i));
+            networksById.put(id, networks.valueAt(i));
+        }
+
+        return networksById;
+    }
+
+    /** Returns a map of network identity token and {@link IpConfiguration}. */
+    public static ArrayMap<String, IpConfiguration> readIpConfigurations(
+            InputStream inputStream) {
+        ArrayMap<String, IpConfiguration> networks = new ArrayMap<>();
+        DataInputStream in = null;
+        try {
+            in = new DataInputStream(inputStream);
+
+            int version = in.readInt();
+            if (version != 3 && version != 2 && version != 1) {
+                loge("Bad version on IP configuration file, ignore read");
+                return null;
+            }
+
+            while (true) {
+                String uniqueToken = null;
+                // Default is DHCP with no proxy
+                IpAssignment ipAssignment = IpAssignment.DHCP;
+                ProxySettings proxySettings = ProxySettings.NONE;
+                StaticIpConfiguration staticIpConfiguration = new StaticIpConfiguration();
+                LinkAddress linkAddress = null;
+                InetAddress gatewayAddress = null;
+                String proxyHost = null;
+                String pacFileUrl = null;
+                int proxyPort = -1;
+                String exclusionList = null;
+                String key;
+                final List<InetAddress> dnsServers = new ArrayList<>();
+
+                do {
+                    key = in.readUTF();
+                    try {
+                        if (key.equals(ID_KEY)) {
+                            if (version < 3) {
+                                int id = in.readInt();
+                                uniqueToken = String.valueOf(id);
+                            } else {
+                                uniqueToken = in.readUTF();
+                            }
+                        } else if (key.equals(IP_ASSIGNMENT_KEY)) {
+                            ipAssignment = IpAssignment.valueOf(in.readUTF());
+                        } else if (key.equals(LINK_ADDRESS_KEY)) {
+                            LinkAddress parsedLinkAddress =
+                                    new LinkAddress(
+                                            InetAddresses.parseNumericAddress(in.readUTF()),
+                                            in.readInt());
+                            if (parsedLinkAddress.getAddress() instanceof Inet4Address
+                                    && linkAddress == null) {
+                                linkAddress = parsedLinkAddress;
+                            } else {
+                                loge("Non-IPv4 or duplicate address: " + parsedLinkAddress);
+                            }
+                        } else if (key.equals(GATEWAY_KEY)) {
+                            LinkAddress dest = null;
+                            InetAddress gateway = null;
+                            if (version == 1) {
+                                // only supported default gateways - leave the dest/prefix empty
+                                gateway = InetAddresses.parseNumericAddress(in.readUTF());
+                                if (gatewayAddress == null) {
+                                    gatewayAddress = gateway;
+                                } else {
+                                    loge("Duplicate gateway: " + gateway.getHostAddress());
+                                }
+                            } else {
+                                if (in.readInt() == 1) {
+                                    dest =
+                                            new LinkAddress(
+                                                    InetAddresses.parseNumericAddress(in.readUTF()),
+                                                    in.readInt());
+                                }
+                                if (in.readInt() == 1) {
+                                    gateway = InetAddresses.parseNumericAddress(in.readUTF());
+                                }
+                                // If the destination is a default IPv4 route, use the gateway
+                                // address unless already set. If there is no destination, assume
+                                // it is default route and use the gateway address in all cases.
+                                if (dest == null) {
+                                    gatewayAddress = gateway;
+                                } else if (dest.getAddress() instanceof Inet4Address
+                                        && dest.getPrefixLength() == 0 && gatewayAddress == null) {
+                                    gatewayAddress = gateway;
+                                } else {
+                                    loge("Non-IPv4 default or duplicate route: "
+                                            + dest.getAddress());
+                                }
+                            }
+                        } else if (key.equals(DNS_KEY)) {
+                            dnsServers.add(InetAddresses.parseNumericAddress(in.readUTF()));
+                        } else if (key.equals(PROXY_SETTINGS_KEY)) {
+                            proxySettings = ProxySettings.valueOf(in.readUTF());
+                        } else if (key.equals(PROXY_HOST_KEY)) {
+                            proxyHost = in.readUTF();
+                        } else if (key.equals(PROXY_PORT_KEY)) {
+                            proxyPort = in.readInt();
+                        } else if (key.equals(PROXY_PAC_FILE)) {
+                            pacFileUrl = in.readUTF();
+                        } else if (key.equals(EXCLUSION_LIST_KEY)) {
+                            exclusionList = in.readUTF();
+                        } else if (key.equals(EOS)) {
+                            break;
+                        } else {
+                            loge("Ignore unknown key " + key + "while reading");
+                        }
+                    } catch (IllegalArgumentException e) {
+                        loge("Ignore invalid address while reading" + e);
+                    }
+                } while (true);
+
+                staticIpConfiguration = new StaticIpConfiguration.Builder()
+                    .setIpAddress(linkAddress)
+                    .setGateway(gatewayAddress)
+                    .setDnsServers(dnsServers)
+                    .build();
+
+                if (uniqueToken != null) {
+                    IpConfiguration config = new IpConfiguration();
+                    networks.put(uniqueToken, config);
+
+                    switch (ipAssignment) {
+                        case STATIC:
+                            config.setStaticIpConfiguration(staticIpConfiguration);
+                            config.setIpAssignment(ipAssignment);
+                            break;
+                        case DHCP:
+                            config.setIpAssignment(ipAssignment);
+                            break;
+                        case UNASSIGNED:
+                            loge("BUG: Found UNASSIGNED IP on file, use DHCP");
+                            config.setIpAssignment(IpAssignment.DHCP);
+                            break;
+                        default:
+                            loge("Ignore invalid ip assignment while reading.");
+                            config.setIpAssignment(IpAssignment.UNASSIGNED);
+                            break;
+                    }
+
+                    switch (proxySettings) {
+                        case STATIC:
+                            ProxyInfo proxyInfo = ProxyInfo.buildDirectProxy(proxyHost, proxyPort,
+                                    ProxyUtils.exclusionStringAsList(exclusionList));
+                            config.setProxySettings(proxySettings);
+                            config.setHttpProxy(proxyInfo);
+                            break;
+                        case PAC:
+                            ProxyInfo proxyPacProperties =
+                                    ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl));
+                            config.setProxySettings(proxySettings);
+                            config.setHttpProxy(proxyPacProperties);
+                            break;
+                        case NONE:
+                            config.setProxySettings(proxySettings);
+                            break;
+                        case UNASSIGNED:
+                            loge("BUG: Found UNASSIGNED proxy on file, use NONE");
+                            config.setProxySettings(ProxySettings.NONE);
+                            break;
+                        default:
+                            loge("Ignore invalid proxy settings while reading");
+                            config.setProxySettings(ProxySettings.UNASSIGNED);
+                            break;
+                    }
+                } else {
+                    if (DBG) log("Missing id while parsing configuration");
+                }
+            }
+        } catch (EOFException ignore) {
+        } catch (IOException e) {
+            loge("Error parsing configuration: " + e);
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (Exception e) { }
+            }
+        }
+
+        return networks;
+    }
+
+    protected static void loge(String s) {
+        Log.e(TAG, s);
+    }
+
+    protected static void log(String s) {
+        Log.d(TAG, s);
+    }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
new file mode 100644
index 0000000..3b93f1a
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.net.NetworkStats.INTERFACES_ALL;
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.TAG_ALL;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.NetworkStats;
+import android.net.UnderlyingNetworkInfo;
+import android.os.ServiceSpecificException;
+import android.os.StrictMode;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ProcFileReader;
+import com.android.net.module.util.CollectionUtils;
+import com.android.server.BpfNetMaps;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Creates {@link NetworkStats} instances by parsing various {@code /proc/}
+ * files as needed.
+ *
+ * @hide
+ */
+public class NetworkStatsFactory {
+    static {
+        System.loadLibrary("service-connectivity");
+    }
+
+    private static final String TAG = "NetworkStatsFactory";
+
+    private static final boolean USE_NATIVE_PARSING = true;
+    private static final boolean VALIDATE_NATIVE_STATS = false;
+
+    /** Path to {@code /proc/net/xt_qtaguid/iface_stat_all}. */
+    private final File mStatsXtIfaceAll;
+    /** Path to {@code /proc/net/xt_qtaguid/iface_stat_fmt}. */
+    private final File mStatsXtIfaceFmt;
+    /** Path to {@code /proc/net/xt_qtaguid/stats}. */
+    private final File mStatsXtUid;
+
+    private final boolean mUseBpfStats;
+
+    private final Context mContext;
+
+    private final BpfNetMaps mBpfNetMaps;
+
+    /**
+     * Guards persistent data access in this class
+     *
+     * <p>In order to prevent deadlocks, critical sections protected by this lock SHALL NOT call out
+     * to other code that will acquire other locks within the system server. See b/134244752.
+     */
+    private final Object mPersistentDataLock = new Object();
+
+    /** Set containing info about active VPNs and their underlying networks. */
+    private volatile UnderlyingNetworkInfo[] mUnderlyingNetworkInfos = new UnderlyingNetworkInfo[0];
+
+    // A persistent snapshot of cumulative stats since device start
+    @GuardedBy("mPersistentDataLock")
+    private NetworkStats mPersistSnapshot;
+
+    // The persistent snapshot of tun and 464xlat adjusted stats since device start
+    @GuardedBy("mPersistentDataLock")
+    private NetworkStats mTunAnd464xlatAdjustedStats;
+
+    /**
+     * (Stacked interface) -> (base interface) association for all connected ifaces since boot.
+     *
+     * Because counters must never roll backwards, once a given interface is stacked on top of an
+     * underlying interface, the stacked interface can never be stacked on top of
+     * another interface. */
+    private final ConcurrentHashMap<String, String> mStackedIfaces
+            = new ConcurrentHashMap<>();
+
+    /** Informs the factory of a new stacked interface. */
+    public void noteStackedIface(String stackedIface, String baseIface) {
+        if (stackedIface != null && baseIface != null) {
+            mStackedIfaces.put(stackedIface, baseIface);
+        }
+    }
+
+    /**
+     * Set active VPN information for data usage migration purposes
+     *
+     * <p>Traffic on TUN-based VPNs inherently all appear to be originated from the VPN providing
+     * app's UID. This method is used to support migration of VPN data usage, ensuring data is
+     * accurately billed to the real owner of the traffic.
+     *
+     * @param vpnArray The snapshot of the currently-running VPNs.
+     */
+    public void updateUnderlyingNetworkInfos(UnderlyingNetworkInfo[] vpnArray) {
+        mUnderlyingNetworkInfos = vpnArray.clone();
+    }
+
+    /**
+     * Get a set of interfaces containing specified ifaces and stacked interfaces.
+     *
+     * <p>The added stacked interfaces are ifaces stacked on top of the specified ones, or ifaces
+     * on which the specified ones are stacked. Stacked interfaces are those noted with
+     * {@link #noteStackedIface(String, String)}, but only interfaces noted before this method
+     * is called are guaranteed to be included.
+     */
+    public String[] augmentWithStackedInterfaces(@Nullable String[] requiredIfaces) {
+        if (requiredIfaces == NetworkStats.INTERFACES_ALL) {
+            return null;
+        }
+
+        HashSet<String> relatedIfaces = new HashSet<>(Arrays.asList(requiredIfaces));
+        // ConcurrentHashMap's EntrySet iterators are "guaranteed to traverse
+        // elements as they existed upon construction exactly once, and may
+        // (but are not guaranteed to) reflect any modifications subsequent to construction".
+        // This is enough here.
+        for (Map.Entry<String, String> entry : mStackedIfaces.entrySet()) {
+            if (relatedIfaces.contains(entry.getKey())) {
+                relatedIfaces.add(entry.getValue());
+            } else if (relatedIfaces.contains(entry.getValue())) {
+                relatedIfaces.add(entry.getKey());
+            }
+        }
+
+        String[] outArray = new String[relatedIfaces.size()];
+        return relatedIfaces.toArray(outArray);
+    }
+
+    /**
+     * Applies 464xlat adjustments with ifaces noted with {@link #noteStackedIface(String, String)}.
+     * @see NetworkStats#apply464xlatAdjustments(NetworkStats, NetworkStats, Map)
+     */
+    public void apply464xlatAdjustments(NetworkStats baseTraffic, NetworkStats stackedTraffic) {
+        NetworkStats.apply464xlatAdjustments(baseTraffic, stackedTraffic, mStackedIfaces);
+    }
+
+    public NetworkStatsFactory(@NonNull Context ctx) {
+        this(ctx, new File("/proc/"), true);
+    }
+
+    @VisibleForTesting
+    public NetworkStatsFactory(@NonNull Context ctx, File procRoot, boolean useBpfStats) {
+        mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
+        mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
+        mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
+        mUseBpfStats = useBpfStats;
+        mBpfNetMaps = new BpfNetMaps();
+        synchronized (mPersistentDataLock) {
+            mPersistSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), -1);
+            mTunAnd464xlatAdjustedStats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
+        }
+        mContext = ctx;
+    }
+
+    public NetworkStats readBpfNetworkStatsDev() throws IOException {
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+        if (nativeReadNetworkStatsDev(stats) != 0) {
+            throw new IOException("Failed to parse bpf iface stats");
+        }
+        return stats;
+    }
+
+    /**
+     * Parse and return interface-level summary {@link NetworkStats} measured
+     * using {@code /proc/net/dev} style hooks, which may include non IP layer
+     * traffic. Values monotonically increase since device boot, and may include
+     * details about inactive interfaces.
+     *
+     * @throws IllegalStateException when problem parsing stats.
+     */
+    public NetworkStats readNetworkStatsSummaryDev() throws IOException {
+
+        // Return xt_bpf stats if switched to bpf module.
+        if (mUseBpfStats)
+            return readBpfNetworkStatsDev();
+
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+        ProcFileReader reader = null;
+        try {
+            reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceAll));
+
+            while (reader.hasMoreData()) {
+                entry.iface = reader.nextString();
+                entry.uid = UID_ALL;
+                entry.set = SET_ALL;
+                entry.tag = TAG_NONE;
+
+                final boolean active = reader.nextInt() != 0;
+
+                // always include snapshot values
+                entry.rxBytes = reader.nextLong();
+                entry.rxPackets = reader.nextLong();
+                entry.txBytes = reader.nextLong();
+                entry.txPackets = reader.nextLong();
+
+                // fold in active numbers, but only when active
+                if (active) {
+                    entry.rxBytes += reader.nextLong();
+                    entry.rxPackets += reader.nextLong();
+                    entry.txBytes += reader.nextLong();
+                    entry.txPackets += reader.nextLong();
+                }
+
+                stats.insertEntry(entry);
+                reader.finishLine();
+            }
+        } catch (NullPointerException|NumberFormatException e) {
+            throw protocolExceptionWithCause("problem parsing stats", e);
+        } finally {
+            IoUtils.closeQuietly(reader);
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+        return stats;
+    }
+
+    /**
+     * Parse and return interface-level summary {@link NetworkStats}. Designed
+     * to return only IP layer traffic. Values monotonically increase since
+     * device boot, and may include details about inactive interfaces.
+     *
+     * @throws IllegalStateException when problem parsing stats.
+     */
+    public NetworkStats readNetworkStatsSummaryXt() throws IOException {
+
+        // Return xt_bpf stats if qtaguid  module is replaced.
+        if (mUseBpfStats)
+            return readBpfNetworkStatsDev();
+
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        // return null when kernel doesn't support
+        if (!mStatsXtIfaceFmt.exists()) return null;
+
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+        ProcFileReader reader = null;
+        try {
+            // open and consume header line
+            reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceFmt));
+            reader.finishLine();
+
+            while (reader.hasMoreData()) {
+                entry.iface = reader.nextString();
+                entry.uid = UID_ALL;
+                entry.set = SET_ALL;
+                entry.tag = TAG_NONE;
+
+                entry.rxBytes = reader.nextLong();
+                entry.rxPackets = reader.nextLong();
+                entry.txBytes = reader.nextLong();
+                entry.txPackets = reader.nextLong();
+
+                stats.insertEntry(entry);
+                reader.finishLine();
+            }
+        } catch (NullPointerException|NumberFormatException e) {
+            throw protocolExceptionWithCause("problem parsing stats", e);
+        } finally {
+            IoUtils.closeQuietly(reader);
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+        return stats;
+    }
+
+    public NetworkStats readNetworkStatsDetail() throws IOException {
+        return readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
+    }
+
+    @GuardedBy("mPersistentDataLock")
+    private void requestSwapActiveStatsMapLocked() throws IOException {
+        try {
+            // Do a active map stats swap. Once the swap completes, this code
+            // can read and clean the inactive map without races.
+            mBpfNetMaps.swapActiveStatsMap();
+        } catch (ServiceSpecificException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Reads the detailed UID stats based on the provided parameters
+     *
+     * @param limitUid the UID to limit this query to
+     * @param limitIfaces the interfaces to limit this query to. Use {@link
+     *     NetworkStats.INTERFACES_ALL} to select all interfaces
+     * @param limitTag the tags to limit this query to
+     * @return the NetworkStats instance containing network statistics at the present time.
+     */
+    public NetworkStats readNetworkStatsDetail(
+            int limitUid, String[] limitIfaces, int limitTag) throws IOException {
+        // In order to prevent deadlocks, anything protected by this lock MUST NOT call out to other
+        // code that will acquire other locks within the system server. See b/134244752.
+        synchronized (mPersistentDataLock) {
+            // Take a reference. If this gets swapped out, we still have the old reference.
+            final UnderlyingNetworkInfo[] vpnArray = mUnderlyingNetworkInfos;
+            // Take a defensive copy. mPersistSnapshot is mutated in some cases below
+            final NetworkStats prev = mPersistSnapshot.clone();
+
+            if (USE_NATIVE_PARSING) {
+                final NetworkStats stats =
+                        new NetworkStats(SystemClock.elapsedRealtime(), 0 /* initialSize */);
+                if (mUseBpfStats) {
+                    requestSwapActiveStatsMapLocked();
+                    // Stats are always read from the inactive map, so they must be read after the
+                    // swap
+                    if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
+                            INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
+                        throw new IOException("Failed to parse network stats");
+                    }
+
+                    // BPF stats are incremental; fold into mPersistSnapshot.
+                    mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime());
+                    mPersistSnapshot.combineAllValues(stats);
+                } else {
+                    if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
+                            INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
+                        throw new IOException("Failed to parse network stats");
+                    }
+                    if (VALIDATE_NATIVE_STATS) {
+                        final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid,
+                                UID_ALL, INTERFACES_ALL, TAG_ALL);
+                        assertEquals(javaStats, stats);
+                    }
+
+                    mPersistSnapshot = stats;
+                }
+            } else {
+                mPersistSnapshot = javaReadNetworkStatsDetail(mStatsXtUid, UID_ALL, INTERFACES_ALL,
+                        TAG_ALL);
+            }
+
+            NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray);
+
+            // Filter return values
+            adjustedStats.filter(limitUid, limitIfaces, limitTag);
+            return adjustedStats;
+        }
+    }
+
+    @GuardedBy("mPersistentDataLock")
+    private NetworkStats adjustForTunAnd464Xlat(NetworkStats uidDetailStats,
+            NetworkStats previousStats, UnderlyingNetworkInfo[] vpnArray) {
+        // Calculate delta from last snapshot
+        final NetworkStats delta = uidDetailStats.subtract(previousStats);
+
+        // Apply 464xlat adjustments before VPN adjustments. If VPNs are using v4 on a v6 only
+        // network, the overhead is their fault.
+        // No locking here: apply464xlatAdjustments behaves fine with an add-only
+        // ConcurrentHashMap.
+        delta.apply464xlatAdjustments(mStackedIfaces);
+
+        // Migrate data usage over a VPN to the TUN network.
+        for (UnderlyingNetworkInfo info : vpnArray) {
+            delta.migrateTun(info.getOwnerUid(), info.getInterface(),
+                    info.getUnderlyingInterfaces());
+            // Filter out debug entries as that may lead to over counting.
+            delta.filterDebugEntries();
+        }
+
+        // Update mTunAnd464xlatAdjustedStats with migrated delta.
+        mTunAnd464xlatAdjustedStats.combineAllValues(delta);
+        mTunAnd464xlatAdjustedStats.setElapsedRealtime(uidDetailStats.getElapsedRealtime());
+
+        return mTunAnd464xlatAdjustedStats.clone();
+    }
+
+    /**
+     * Parse and return {@link NetworkStats} with UID-level details. Values are
+     * expected to monotonically increase since device boot.
+     */
+    @VisibleForTesting
+    public static NetworkStats javaReadNetworkStatsDetail(File detailPath, int limitUid,
+            String[] limitIfaces, int limitTag)
+            throws IOException {
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 24);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+        int idx = 1;
+        int lastIdx = 1;
+
+        ProcFileReader reader = null;
+        try {
+            // open and consume header line
+            reader = new ProcFileReader(new FileInputStream(detailPath));
+            reader.finishLine();
+
+            while (reader.hasMoreData()) {
+                idx = reader.nextInt();
+                if (idx != lastIdx + 1) {
+                    throw new ProtocolException(
+                            "inconsistent idx=" + idx + " after lastIdx=" + lastIdx);
+                }
+                lastIdx = idx;
+
+                entry.iface = reader.nextString();
+                entry.tag = kernelToTag(reader.nextString());
+                entry.uid = reader.nextInt();
+                entry.set = reader.nextInt();
+                entry.rxBytes = reader.nextLong();
+                entry.rxPackets = reader.nextLong();
+                entry.txBytes = reader.nextLong();
+                entry.txPackets = reader.nextLong();
+
+                if ((limitIfaces == null || CollectionUtils.contains(limitIfaces, entry.iface))
+                        && (limitUid == UID_ALL || limitUid == entry.uid)
+                        && (limitTag == TAG_ALL || limitTag == entry.tag)) {
+                    stats.insertEntry(entry);
+                }
+
+                reader.finishLine();
+            }
+        } catch (NullPointerException|NumberFormatException e) {
+            throw protocolExceptionWithCause("problem parsing idx " + idx, e);
+        } finally {
+            IoUtils.closeQuietly(reader);
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+
+        return stats;
+    }
+
+    public void assertEquals(NetworkStats expected, NetworkStats actual) {
+        if (expected.size() != actual.size()) {
+            throw new AssertionError(
+                    "Expected size " + expected.size() + ", actual size " + actual.size());
+        }
+
+        NetworkStats.Entry expectedRow = null;
+        NetworkStats.Entry actualRow = null;
+        for (int i = 0; i < expected.size(); i++) {
+            expectedRow = expected.getValues(i, expectedRow);
+            actualRow = actual.getValues(i, actualRow);
+            if (!expectedRow.equals(actualRow)) {
+                throw new AssertionError(
+                        "Expected row " + i + ": " + expectedRow + ", actual row " + actualRow);
+            }
+        }
+    }
+
+    /**
+     * Convert {@code /proc/} tag format to {@link Integer}. Assumes incoming
+     * format like {@code 0x7fffffff00000000}.
+     */
+    public static int kernelToTag(String string) {
+        int length = string.length();
+        if (length > 10) {
+            return Long.decode(string.substring(0, length - 8)).intValue();
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Parse statistics from file into given {@link NetworkStats} object. Values
+     * are expected to monotonically increase since device boot.
+     */
+    @VisibleForTesting
+    public static native int nativeReadNetworkStatsDetail(NetworkStats stats, String path,
+        int limitUid, String[] limitIfaces, int limitTag, boolean useBpfStats);
+
+    @VisibleForTesting
+    public static native int nativeReadNetworkStatsDev(NetworkStats stats);
+
+    private static ProtocolException protocolExceptionWithCause(String message, Throwable cause) {
+        ProtocolException pe = new ProtocolException(message);
+        pe.initCause(cause);
+        return pe;
+    }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
new file mode 100644
index 0000000..fdfc893
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
+
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.DataUsageRequest;
+import android.net.NetworkIdentitySet;
+import android.net.NetworkStack;
+import android.net.NetworkStats;
+import android.net.NetworkStatsAccess;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.netstats.IUsageCallback;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Manages observers of {@link NetworkStats}. Allows observers to be notified when
+ * data usage has been reported in {@link NetworkStatsService}. An observer can set
+ * a threshold of how much data it cares about to be notified.
+ */
+class NetworkStatsObservers {
+    private static final String TAG = "NetworkStatsObservers";
+    private static final boolean LOGV = false;
+
+    private static final int MSG_REGISTER = 1;
+    private static final int MSG_UNREGISTER = 2;
+    private static final int MSG_UPDATE_STATS = 3;
+
+    // All access to this map must be done from the handler thread.
+    // indexed by DataUsageRequest#requestId
+    private final SparseArray<RequestInfo> mDataUsageRequests = new SparseArray<>();
+
+    // Sequence number of DataUsageRequests
+    private final AtomicInteger mNextDataUsageRequestId = new AtomicInteger();
+
+    // Lazily instantiated when an observer is registered.
+    private volatile Handler mHandler;
+
+    /**
+     * Creates a wrapper that contains the caller context and a normalized request.
+     * The request should be returned to the caller app, and the wrapper should be sent to this
+     * object through #addObserver by the service handler.
+     *
+     * <p>It will register the observer asynchronously, so it is safe to call from any thread.
+     *
+     * @return the normalized request wrapped within {@link RequestInfo}.
+     */
+    public DataUsageRequest register(Context context, DataUsageRequest inputRequest,
+            IUsageCallback callback, int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+        DataUsageRequest request = buildRequest(context, inputRequest, callingUid);
+        RequestInfo requestInfo = buildRequestInfo(request, callback, callingUid,
+                accessLevel);
+
+        if (LOGV) Log.v(TAG, "Registering observer for " + request);
+        getHandler().sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
+        return request;
+    }
+
+    /**
+     * Unregister a data usage observer.
+     *
+     * <p>It will unregister the observer asynchronously, so it is safe to call from any thread.
+     */
+    public void unregister(DataUsageRequest request, int callingUid) {
+        getHandler().sendMessage(mHandler.obtainMessage(MSG_UNREGISTER, callingUid, 0 /* ignore */,
+                request));
+    }
+
+    /**
+     * Updates data usage statistics of registered observers and notifies if limits are reached.
+     *
+     * <p>It will update stats asynchronously, so it is safe to call from any thread.
+     */
+    public void updateStats(NetworkStats xtSnapshot, NetworkStats uidSnapshot,
+                ArrayMap<String, NetworkIdentitySet> activeIfaces,
+                ArrayMap<String, NetworkIdentitySet> activeUidIfaces,
+                long currentTime) {
+        StatsContext statsContext = new StatsContext(xtSnapshot, uidSnapshot, activeIfaces,
+                activeUidIfaces, currentTime);
+        getHandler().sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATS, statsContext));
+    }
+
+    private Handler getHandler() {
+        if (mHandler == null) {
+            synchronized (this) {
+                if (mHandler == null) {
+                    if (LOGV) Log.v(TAG, "Creating handler");
+                    mHandler = new Handler(getHandlerLooperLocked(), mHandlerCallback);
+                }
+            }
+        }
+        return mHandler;
+    }
+
+    @VisibleForTesting
+    protected Looper getHandlerLooperLocked() {
+        HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        return handlerThread.getLooper();
+    }
+
+    private Handler.Callback mHandlerCallback = new Handler.Callback() {
+        @Override
+        public boolean handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_REGISTER: {
+                    handleRegister((RequestInfo) msg.obj);
+                    return true;
+                }
+                case MSG_UNREGISTER: {
+                    handleUnregister((DataUsageRequest) msg.obj, msg.arg1 /* callingUid */);
+                    return true;
+                }
+                case MSG_UPDATE_STATS: {
+                    handleUpdateStats((StatsContext) msg.obj);
+                    return true;
+                }
+                default: {
+                    return false;
+                }
+            }
+        }
+    };
+
+    /**
+     * Adds a {@link RequestInfo} as an observer.
+     * Should only be called from the handler thread otherwise there will be a race condition
+     * on mDataUsageRequests.
+     */
+    private void handleRegister(RequestInfo requestInfo) {
+        mDataUsageRequests.put(requestInfo.mRequest.requestId, requestInfo);
+    }
+
+    /**
+     * Removes a {@link DataUsageRequest} if the calling uid is authorized.
+     * Should only be called from the handler thread otherwise there will be a race condition
+     * on mDataUsageRequests.
+     */
+    private void handleUnregister(DataUsageRequest request, int callingUid) {
+        RequestInfo requestInfo;
+        requestInfo = mDataUsageRequests.get(request.requestId);
+        if (requestInfo == null) {
+            if (LOGV) Log.v(TAG, "Trying to unregister unknown request " + request);
+            return;
+        }
+        if (Process.SYSTEM_UID != callingUid && requestInfo.mCallingUid != callingUid) {
+            Log.w(TAG, "Caller uid " + callingUid + " is not owner of " + request);
+            return;
+        }
+
+        if (LOGV) Log.v(TAG, "Unregistering " + request);
+        mDataUsageRequests.remove(request.requestId);
+        requestInfo.unlinkDeathRecipient();
+        requestInfo.callCallback(NetworkStatsManager.CALLBACK_RELEASED);
+    }
+
+    private void handleUpdateStats(StatsContext statsContext) {
+        if (mDataUsageRequests.size() == 0) {
+            return;
+        }
+
+        for (int i = 0; i < mDataUsageRequests.size(); i++) {
+            RequestInfo requestInfo = mDataUsageRequests.valueAt(i);
+            requestInfo.updateStats(statsContext);
+        }
+    }
+
+    private DataUsageRequest buildRequest(Context context, DataUsageRequest request,
+                int callingUid) {
+        // For non-NETWORK_STACK permission uid, cap the minimum threshold to a safe default to
+        // avoid too many callbacks.
+        final long thresholdInBytes = (context.checkPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, Process.myPid(), callingUid)
+                == PackageManager.PERMISSION_GRANTED ? request.thresholdInBytes
+                : Math.max(MIN_THRESHOLD_BYTES, request.thresholdInBytes));
+        if (thresholdInBytes > request.thresholdInBytes) {
+            Log.w(TAG, "Threshold was too low for " + request
+                    + ". Overriding to a safer default of " + thresholdInBytes + " bytes");
+        }
+        return new DataUsageRequest(mNextDataUsageRequestId.incrementAndGet(),
+                request.template, thresholdInBytes);
+    }
+
+    private RequestInfo buildRequestInfo(DataUsageRequest request, IUsageCallback callback,
+            int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+        if (accessLevel <= NetworkStatsAccess.Level.USER) {
+            return new UserUsageRequestInfo(this, request, callback, callingUid,
+                    accessLevel);
+        } else {
+            // Safety check in case a new access level is added and we forgot to update this
+            if (accessLevel < NetworkStatsAccess.Level.DEVICESUMMARY) {
+                throw new IllegalArgumentException(
+                        "accessLevel " + accessLevel + " is less than DEVICESUMMARY.");
+            }
+            return new NetworkUsageRequestInfo(this, request, callback, callingUid,
+                    accessLevel);
+        }
+    }
+
+    /**
+     * Tracks information relevant to a data usage observer.
+     * It will notice when the calling process dies so we can self-expire.
+     */
+    private abstract static class RequestInfo implements IBinder.DeathRecipient {
+        private final NetworkStatsObservers mStatsObserver;
+        protected final DataUsageRequest mRequest;
+        private final IUsageCallback mCallback;
+        protected final int mCallingUid;
+        protected final @NetworkStatsAccess.Level int mAccessLevel;
+        protected NetworkStatsRecorder mRecorder;
+        protected NetworkStatsCollection mCollection;
+
+        RequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
+                IUsageCallback callback, int callingUid,
+                    @NetworkStatsAccess.Level int accessLevel) {
+            mStatsObserver = statsObserver;
+            mRequest = request;
+            mCallback = callback;
+            mCallingUid = callingUid;
+            mAccessLevel = accessLevel;
+
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                binderDied();
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            if (LOGV) {
+                Log.v(TAG, "RequestInfo binderDied(" + mRequest + ", " + mCallback + ")");
+            }
+            mStatsObserver.unregister(mRequest, Process.SYSTEM_UID);
+            callCallback(NetworkStatsManager.CALLBACK_RELEASED);
+        }
+
+        @Override
+        public String toString() {
+            return "RequestInfo from uid:" + mCallingUid
+                    + " for " + mRequest + " accessLevel:" + mAccessLevel;
+        }
+
+        private void unlinkDeathRecipient() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        /**
+         * Update stats given the samples and interface to identity mappings.
+         */
+        private void updateStats(StatsContext statsContext) {
+            if (mRecorder == null) {
+                // First run; establish baseline stats
+                resetRecorder();
+                recordSample(statsContext);
+                return;
+            }
+            recordSample(statsContext);
+
+            if (checkStats()) {
+                resetRecorder();
+                callCallback(NetworkStatsManager.CALLBACK_LIMIT_REACHED);
+            }
+        }
+
+        private void callCallback(int callbackType) {
+            try {
+                if (LOGV) {
+                    Log.v(TAG, "sending notification " + callbackTypeToName(callbackType)
+                            + " for " + mRequest);
+                }
+                switch (callbackType) {
+                    case NetworkStatsManager.CALLBACK_LIMIT_REACHED:
+                        mCallback.onThresholdReached(mRequest);
+                        break;
+                    case NetworkStatsManager.CALLBACK_RELEASED:
+                        mCallback.onCallbackReleased(mRequest);
+                        break;
+                }
+            } catch (RemoteException e) {
+                // May occur naturally in the race of binder death.
+                Log.w(TAG, "RemoteException caught trying to send a callback msg for " + mRequest);
+            }
+        }
+
+        private void resetRecorder() {
+            mRecorder = new NetworkStatsRecorder();
+            mCollection = mRecorder.getSinceBoot();
+        }
+
+        protected abstract boolean checkStats();
+
+        protected abstract void recordSample(StatsContext statsContext);
+
+        private String callbackTypeToName(int callbackType) {
+            switch (callbackType) {
+                case NetworkStatsManager.CALLBACK_LIMIT_REACHED:
+                    return "LIMIT_REACHED";
+                case NetworkStatsManager.CALLBACK_RELEASED:
+                    return "RELEASED";
+                default:
+                    return "UNKNOWN";
+            }
+        }
+    }
+
+    private static class NetworkUsageRequestInfo extends RequestInfo {
+        NetworkUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
+                IUsageCallback callback, int callingUid,
+                    @NetworkStatsAccess.Level int accessLevel) {
+            super(statsObserver, request, callback, callingUid, accessLevel);
+        }
+
+        @Override
+        protected boolean checkStats() {
+            long bytesSoFar = getTotalBytesForNetwork(mRequest.template);
+            if (LOGV) {
+                Log.v(TAG, bytesSoFar + " bytes so far since notification for "
+                        + mRequest.template);
+            }
+            if (bytesSoFar > mRequest.thresholdInBytes) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        protected void recordSample(StatsContext statsContext) {
+            // Recorder does not need to be locked in this context since only the handler
+            // thread will update it. We pass a null VPN array because usage is aggregated by uid
+            // for this snapshot, so VPN traffic can't be reattributed to responsible apps.
+            mRecorder.recordSnapshotLocked(statsContext.mXtSnapshot, statsContext.mActiveIfaces,
+                    statsContext.mCurrentTime);
+        }
+
+        /**
+         * Reads stats matching the given template. {@link NetworkStatsCollection} will aggregate
+         * over all buckets, which in this case should be only one since we built it big enough
+         * that it will outlive the caller. If it doesn't, then there will be multiple buckets.
+         */
+        private long getTotalBytesForNetwork(NetworkTemplate template) {
+            NetworkStats stats = mCollection.getSummary(template,
+                    Long.MIN_VALUE /* start */, Long.MAX_VALUE /* end */,
+                    mAccessLevel, mCallingUid);
+            return stats.getTotalBytes();
+        }
+    }
+
+    private static class UserUsageRequestInfo extends RequestInfo {
+        UserUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
+                    IUsageCallback callback, int callingUid,
+                    @NetworkStatsAccess.Level int accessLevel) {
+            super(statsObserver, request, callback, callingUid, accessLevel);
+        }
+
+        @Override
+        protected boolean checkStats() {
+            int[] uidsToMonitor = mCollection.getRelevantUids(mAccessLevel, mCallingUid);
+
+            for (int i = 0; i < uidsToMonitor.length; i++) {
+                long bytesSoFar = getTotalBytesForNetworkUid(mRequest.template, uidsToMonitor[i]);
+                if (bytesSoFar > mRequest.thresholdInBytes) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        protected void recordSample(StatsContext statsContext) {
+            // Recorder does not need to be locked in this context since only the handler
+            // thread will update it. We pass the VPN info so VPN traffic is reattributed to
+            // responsible apps.
+            mRecorder.recordSnapshotLocked(statsContext.mUidSnapshot, statsContext.mActiveUidIfaces,
+                    statsContext.mCurrentTime);
+        }
+
+        /**
+         * Reads all stats matching the given template and uid. Ther history will likely only
+         * contain one bucket per ident since we build it big enough that it will outlive the
+         * caller lifetime.
+         */
+        private long getTotalBytesForNetworkUid(NetworkTemplate template, int uid) {
+            try {
+                NetworkStatsHistory history = mCollection.getHistory(template, null, uid,
+                        NetworkStats.SET_ALL, NetworkStats.TAG_NONE,
+                        NetworkStatsHistory.FIELD_ALL,
+                        Long.MIN_VALUE /* start */, Long.MAX_VALUE /* end */,
+                        mAccessLevel, mCallingUid);
+                return history.getTotalBytes();
+            } catch (SecurityException e) {
+                if (LOGV) {
+                    Log.w(TAG, "CallerUid " + mCallingUid + " may have lost access to uid "
+                            + uid);
+                }
+                return 0;
+            }
+        }
+    }
+
+    private static class StatsContext {
+        NetworkStats mXtSnapshot;
+        NetworkStats mUidSnapshot;
+        ArrayMap<String, NetworkIdentitySet> mActiveIfaces;
+        ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
+        long mCurrentTime;
+
+        StatsContext(NetworkStats xtSnapshot, NetworkStats uidSnapshot,
+                ArrayMap<String, NetworkIdentitySet> activeIfaces,
+                ArrayMap<String, NetworkIdentitySet> activeUidIfaces,
+                long currentTime) {
+            mXtSnapshot = xtSnapshot;
+            mUidSnapshot = uidSnapshot;
+            mActiveIfaces = activeIfaces;
+            mActiveUidIfaces = activeUidIfaces;
+            mCurrentTime = currentTime;
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
new file mode 100644
index 0000000..f62765d
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.TrafficStats.KB_IN_BYTES;
+import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+
+import android.net.NetworkIdentitySet;
+import android.net.NetworkStats;
+import android.net.NetworkStats.NonMonotonicObserver;
+import android.net.NetworkStatsAccess;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TrafficStats;
+import android.os.Binder;
+import android.os.DropBoxManager;
+import android.service.NetworkStatsRecorderProto;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.FileRotator;
+import com.android.net.module.util.NetworkStatsUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Logic to record deltas between periodic {@link NetworkStats} snapshots into
+ * {@link NetworkStatsHistory} that belong to {@link NetworkStatsCollection}.
+ * Keeps pending changes in memory until they pass a specific threshold, in
+ * bytes. Uses {@link FileRotator} for persistence logic if present.
+ * <p>
+ * Not inherently thread safe.
+ */
+public class NetworkStatsRecorder {
+    private static final String TAG = "NetworkStatsRecorder";
+    private static final boolean LOGD = false;
+    private static final boolean LOGV = false;
+
+    private static final String TAG_NETSTATS_DUMP = "netstats_dump";
+
+    /** Dump before deleting in {@link #recoverFromWtf()}. */
+    private static final boolean DUMP_BEFORE_DELETE = true;
+
+    private final FileRotator mRotator;
+    private final NonMonotonicObserver<String> mObserver;
+    private final DropBoxManager mDropBox;
+    private final String mCookie;
+
+    private final long mBucketDuration;
+    private final boolean mOnlyTags;
+
+    private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
+    private NetworkStats mLastSnapshot;
+
+    private final NetworkStatsCollection mPending;
+    private final NetworkStatsCollection mSinceBoot;
+
+    private final CombiningRewriter mPendingRewriter;
+
+    private WeakReference<NetworkStatsCollection> mComplete;
+
+    /**
+     * Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
+     */
+    public NetworkStatsRecorder() {
+        mRotator = null;
+        mObserver = null;
+        mDropBox = null;
+        mCookie = null;
+
+        // set the bucket big enough to have all data in one bucket, but allow some
+        // slack to avoid overflow
+        mBucketDuration = YEAR_IN_MILLIS;
+        mOnlyTags = false;
+
+        mPending = null;
+        mSinceBoot = new NetworkStatsCollection(mBucketDuration);
+
+        mPendingRewriter = null;
+    }
+
+    /**
+     * Persisted recorder.
+     */
+    public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
+            DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
+        mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
+        mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
+        mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
+        mCookie = cookie;
+
+        mBucketDuration = bucketDuration;
+        mOnlyTags = onlyTags;
+
+        mPending = new NetworkStatsCollection(bucketDuration);
+        mSinceBoot = new NetworkStatsCollection(bucketDuration);
+
+        mPendingRewriter = new CombiningRewriter(mPending);
+    }
+
+    public void setPersistThreshold(long thresholdBytes) {
+        if (LOGV) Log.v(TAG, "setPersistThreshold() with " + thresholdBytes);
+        mPersistThresholdBytes = NetworkStatsUtils.constrain(
+                thresholdBytes, 1 * KB_IN_BYTES, 100 * MB_IN_BYTES);
+    }
+
+    public void resetLocked() {
+        mLastSnapshot = null;
+        if (mPending != null) {
+            mPending.reset();
+        }
+        if (mSinceBoot != null) {
+            mSinceBoot.reset();
+        }
+        if (mComplete != null) {
+            mComplete.clear();
+        }
+    }
+
+    public NetworkStats.Entry getTotalSinceBootLocked(NetworkTemplate template) {
+        return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE,
+                NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotal(null);
+    }
+
+    public NetworkStatsCollection getSinceBoot() {
+        return mSinceBoot;
+    }
+
+    /**
+     * Load complete history represented by {@link FileRotator}. Caches
+     * internally as a {@link WeakReference}, and updated with future
+     * {@link #recordSnapshotLocked(NetworkStats, Map, long)} snapshots as long
+     * as reference is valid.
+     */
+    public NetworkStatsCollection getOrLoadCompleteLocked() {
+        Objects.requireNonNull(mRotator, "missing FileRotator");
+        NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
+        if (res == null) {
+            res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+            mComplete = new WeakReference<NetworkStatsCollection>(res);
+        }
+        return res;
+    }
+
+    public NetworkStatsCollection getOrLoadPartialLocked(long start, long end) {
+        Objects.requireNonNull(mRotator, "missing FileRotator");
+        NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
+        if (res == null) {
+            res = loadLocked(start, end);
+        }
+        return res;
+    }
+
+    private NetworkStatsCollection loadLocked(long start, long end) {
+        if (LOGD) Log.d(TAG, "loadLocked() reading from disk for " + mCookie);
+        final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
+        try {
+            mRotator.readMatching(res, start, end);
+            res.recordCollection(mPending);
+        } catch (IOException e) {
+            Log.wtf(TAG, "problem completely reading network stats", e);
+            recoverFromWtf();
+        } catch (OutOfMemoryError e) {
+            Log.wtf(TAG, "problem completely reading network stats", e);
+            recoverFromWtf();
+        }
+        return res;
+    }
+
+    /**
+     * Record any delta that occurred since last {@link NetworkStats} snapshot, using the given
+     * {@link Map} to identify network interfaces. First snapshot is considered bootstrap, and is
+     * not counted as delta.
+     */
+    public void recordSnapshotLocked(NetworkStats snapshot,
+            Map<String, NetworkIdentitySet> ifaceIdent, long currentTimeMillis) {
+        final HashSet<String> unknownIfaces = new HashSet<>();
+
+        // skip recording when snapshot missing
+        if (snapshot == null) return;
+
+        // assume first snapshot is bootstrap and don't record
+        if (mLastSnapshot == null) {
+            mLastSnapshot = snapshot;
+            return;
+        }
+
+        final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+
+        final NetworkStats delta = NetworkStats.subtract(
+                snapshot, mLastSnapshot, mObserver, mCookie);
+        final long end = currentTimeMillis;
+        final long start = end - delta.getElapsedRealtime();
+
+        NetworkStats.Entry entry = null;
+        for (int i = 0; i < delta.size(); i++) {
+            entry = delta.getValues(i, entry);
+
+            // As a last-ditch check, report any negative values and
+            // clamp them so recording below doesn't croak.
+            if (entry.isNegative()) {
+                if (mObserver != null) {
+                    mObserver.foundNonMonotonic(delta, i, mCookie);
+                }
+                entry.rxBytes = Math.max(entry.rxBytes, 0);
+                entry.rxPackets = Math.max(entry.rxPackets, 0);
+                entry.txBytes = Math.max(entry.txBytes, 0);
+                entry.txPackets = Math.max(entry.txPackets, 0);
+                entry.operations = Math.max(entry.operations, 0);
+            }
+
+            final NetworkIdentitySet ident = ifaceIdent.get(entry.iface);
+            if (ident == null) {
+                unknownIfaces.add(entry.iface);
+                continue;
+            }
+
+            // skip when no delta occurred
+            if (entry.isEmpty()) continue;
+
+            // only record tag data when requested
+            if ((entry.tag == TAG_NONE) != mOnlyTags) {
+                if (mPending != null) {
+                    mPending.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+                }
+
+                // also record against boot stats when present
+                if (mSinceBoot != null) {
+                    mSinceBoot.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+                }
+
+                // also record against complete dataset when present
+                if (complete != null) {
+                    complete.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+                }
+            }
+        }
+
+        mLastSnapshot = snapshot;
+
+        if (LOGV && unknownIfaces.size() > 0) {
+            Log.w(TAG, "unknown interfaces " + unknownIfaces + ", ignoring those stats");
+        }
+    }
+
+    /**
+     * Consider persisting any pending deltas, if they are beyond
+     * {@link #mPersistThresholdBytes}.
+     */
+    public void maybePersistLocked(long currentTimeMillis) {
+        Objects.requireNonNull(mRotator, "missing FileRotator");
+        final long pendingBytes = mPending.getTotalBytes();
+        if (pendingBytes >= mPersistThresholdBytes) {
+            forcePersistLocked(currentTimeMillis);
+        } else {
+            mRotator.maybeRotate(currentTimeMillis);
+        }
+    }
+
+    /**
+     * Force persisting any pending deltas.
+     */
+    public void forcePersistLocked(long currentTimeMillis) {
+        Objects.requireNonNull(mRotator, "missing FileRotator");
+        if (mPending.isDirty()) {
+            if (LOGD) Log.d(TAG, "forcePersistLocked() writing for " + mCookie);
+            try {
+                mRotator.rewriteActive(mPendingRewriter, currentTimeMillis);
+                mRotator.maybeRotate(currentTimeMillis);
+                mPending.reset();
+            } catch (IOException e) {
+                Log.wtf(TAG, "problem persisting pending stats", e);
+                recoverFromWtf();
+            } catch (OutOfMemoryError e) {
+                Log.wtf(TAG, "problem persisting pending stats", e);
+                recoverFromWtf();
+            }
+        }
+    }
+
+    /**
+     * Remove the given UID from all {@link FileRotator} history, migrating it
+     * to {@link TrafficStats#UID_REMOVED}.
+     */
+    public void removeUidsLocked(int[] uids) {
+        if (mRotator != null) {
+            try {
+                // Rewrite all persisted data to migrate UID stats
+                mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
+            } catch (IOException e) {
+                Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
+                recoverFromWtf();
+            } catch (OutOfMemoryError e) {
+                Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
+                recoverFromWtf();
+            }
+        }
+
+        // Remove any pending stats
+        if (mPending != null) {
+            mPending.removeUids(uids);
+        }
+        if (mSinceBoot != null) {
+            mSinceBoot.removeUids(uids);
+        }
+
+        // Clear UID from current stats snapshot
+        if (mLastSnapshot != null) {
+            mLastSnapshot.removeUids(uids);
+        }
+
+        final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+        if (complete != null) {
+            complete.removeUids(uids);
+        }
+    }
+
+    /**
+     * Rewriter that will combine current {@link NetworkStatsCollection} values
+     * with anything read from disk, and write combined set to disk. Clears the
+     * original {@link NetworkStatsCollection} when finished writing.
+     */
+    private static class CombiningRewriter implements FileRotator.Rewriter {
+        private final NetworkStatsCollection mCollection;
+
+        public CombiningRewriter(NetworkStatsCollection collection) {
+            mCollection = Objects.requireNonNull(collection, "missing NetworkStatsCollection");
+        }
+
+        @Override
+        public void reset() {
+            // ignored
+        }
+
+        @Override
+        public void read(InputStream in) throws IOException {
+            mCollection.read(in);
+        }
+
+        @Override
+        public boolean shouldWrite() {
+            return true;
+        }
+
+        @Override
+        public void write(OutputStream out) throws IOException {
+            mCollection.write(out);
+            mCollection.reset();
+        }
+    }
+
+    /**
+     * Rewriter that will remove any {@link NetworkStatsHistory} attributed to
+     * the requested UID, only writing data back when modified.
+     */
+    public static class RemoveUidRewriter implements FileRotator.Rewriter {
+        private final NetworkStatsCollection mTemp;
+        private final int[] mUids;
+
+        public RemoveUidRewriter(long bucketDuration, int[] uids) {
+            mTemp = new NetworkStatsCollection(bucketDuration);
+            mUids = uids;
+        }
+
+        @Override
+        public void reset() {
+            mTemp.reset();
+        }
+
+        @Override
+        public void read(InputStream in) throws IOException {
+            mTemp.read(in);
+            mTemp.clearDirty();
+            mTemp.removeUids(mUids);
+        }
+
+        @Override
+        public boolean shouldWrite() {
+            return mTemp.isDirty();
+        }
+
+        @Override
+        public void write(OutputStream out) throws IOException {
+            mTemp.write(out);
+        }
+    }
+
+    public void importLegacyNetworkLocked(File file) throws IOException {
+        Objects.requireNonNull(mRotator, "missing FileRotator");
+
+        // legacy file still exists; start empty to avoid double importing
+        mRotator.deleteAll();
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
+        collection.readLegacyNetwork(file);
+
+        final long startMillis = collection.getStartMillis();
+        final long endMillis = collection.getEndMillis();
+
+        if (!collection.isEmpty()) {
+            // process legacy data, creating active file at starting time, then
+            // using end time to possibly trigger rotation.
+            mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
+            mRotator.maybeRotate(endMillis);
+        }
+    }
+
+    public void importLegacyUidLocked(File file) throws IOException {
+        Objects.requireNonNull(mRotator, "missing FileRotator");
+
+        // legacy file still exists; start empty to avoid double importing
+        mRotator.deleteAll();
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
+        collection.readLegacyUid(file, mOnlyTags);
+
+        final long startMillis = collection.getStartMillis();
+        final long endMillis = collection.getEndMillis();
+
+        if (!collection.isEmpty()) {
+            // process legacy data, creating active file at starting time, then
+            // using end time to possibly trigger rotation.
+            mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
+            mRotator.maybeRotate(endMillis);
+        }
+    }
+
+    public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) {
+        if (mPending != null) {
+            pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes());
+        }
+        if (fullHistory) {
+            pw.println("Complete history:");
+            getOrLoadCompleteLocked().dump(pw);
+        } else {
+            pw.println("History since boot:");
+            mSinceBoot.dump(pw);
+        }
+    }
+
+    public void dumpDebugLocked(ProtoOutputStream proto, long tag) {
+        final long start = proto.start(tag);
+        if (mPending != null) {
+            proto.write(NetworkStatsRecorderProto.PENDING_TOTAL_BYTES,
+                    mPending.getTotalBytes());
+        }
+        getOrLoadCompleteLocked().dumpDebug(proto,
+                NetworkStatsRecorderProto.COMPLETE_HISTORY);
+        proto.end(start);
+    }
+
+    public void dumpCheckin(PrintWriter pw, long start, long end) {
+        // Only load and dump stats from the requested window
+        getOrLoadPartialLocked(start, end).dumpCheckin(pw, start, end);
+    }
+
+    /**
+     * Recover from {@link FileRotator} failure by dumping state to
+     * {@link DropBoxManager} and deleting contents.
+     */
+    private void recoverFromWtf() {
+        if (DUMP_BEFORE_DELETE) {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            try {
+                mRotator.dumpAll(os);
+            } catch (IOException e) {
+                // ignore partial contents
+                os.reset();
+            } finally {
+                IoUtils.closeQuietly(os);
+            }
+            mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
+        }
+
+        mRotator.deleteAll();
+    }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
new file mode 100644
index 0000000..e3794e4
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -0,0 +1,2528 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.Manifest.permission.NETWORK_STATS_PROVIDER;
+import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
+import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.app.usage.NetworkStatsManager.PREFIX_DEV;
+import static android.content.Intent.ACTION_SHUTDOWN;
+import static android.content.Intent.ACTION_UID_REMOVED;
+import static android.content.Intent.ACTION_USER_REMOVED;
+import static android.content.Intent.EXTRA_UID;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
+import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.IFACE_VT;
+import static android.net.NetworkStats.INTERFACES_ALL;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.ROAMING_ALL;
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.STATS_PER_IFACE;
+import static android.net.NetworkStats.STATS_PER_UID;
+import static android.net.NetworkStats.TAG_ALL;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStatsHistory.FIELD_ALL;
+import static android.net.NetworkTemplate.buildTemplateMobileWildcard;
+import static android.net.NetworkTemplate.buildTemplateWifiWildcard;
+import static android.net.TrafficStats.KB_IN_BYTES;
+import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.TrafficStats.UNSUPPORTED;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+import static android.os.Trace.TRACE_TAG_NETWORK;
+import static android.system.OsConstants.ENOENT;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
+import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TargetApi;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.usage.NetworkStatsManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.net.DataUsageRequest;
+import android.net.INetd;
+import android.net.INetworkStatsService;
+import android.net.INetworkStatsSession;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
+import android.net.NetworkIdentitySet;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkSpecifier;
+import android.net.NetworkStack;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkStats;
+import android.net.NetworkStats.NonMonotonicObserver;
+import android.net.NetworkStatsAccess;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.TetherStatsParcel;
+import android.net.TetheringManager;
+import android.net.TrafficStats;
+import android.net.UnderlyingNetworkInfo;
+import android.net.Uri;
+import android.net.netstats.IUsageCallback;
+import android.net.netstats.provider.INetworkStatsProvider;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.net.netstats.provider.NetworkStatsProvider;
+import android.os.Binder;
+import android.os.Build;
+import android.os.DropBoxManager;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.service.NetworkInterfaceProto;
+import android.service.NetworkStatsServiceDumpProto;
+import android.system.ErrnoException;
+import android.telephony.PhoneStateListener;
+import android.telephony.SubscriptionPlan;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.EventLog;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FileRotator;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.BestClock;
+import com.android.net.module.util.BinderUtils;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.LocationPermissionChecker;
+import com.android.net.module.util.NetworkStatsUtils;
+import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.time.Clock;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Collect and persist detailed network statistics, and provide this data to
+ * other system services.
+ */
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class NetworkStatsService extends INetworkStatsService.Stub {
+    static {
+        System.loadLibrary("service-connectivity");
+    }
+
+    static final String TAG = "NetworkStats";
+    static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
+    static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
+
+    // Perform polling and persist all (FLAG_PERSIST_ALL).
+    private static final int MSG_PERFORM_POLL = 1;
+    // Perform polling, persist network, and register the global alert again.
+    private static final int MSG_PERFORM_POLL_REGISTER_ALERT = 2;
+    private static final int MSG_NOTIFY_NETWORK_STATUS = 3;
+    // A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent
+    // deadlock.
+    private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4;
+
+    /** Flags to control detail level of poll event. */
+    private static final int FLAG_PERSIST_NETWORK = 0x1;
+    private static final int FLAG_PERSIST_UID = 0x2;
+    private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID;
+    private static final int FLAG_PERSIST_FORCE = 0x100;
+
+    /**
+     * When global alert quota is high, wait for this delay before processing each polling,
+     * and do not schedule further polls once there is already one queued.
+     * This avoids firing the global alert too often on devices with high transfer speeds and
+     * high quota.
+     */
+    private static final int DEFAULT_PERFORM_POLL_DELAY_MS = 1000;
+
+    private static final String TAG_NETSTATS_ERROR = "netstats_error";
+
+    /**
+     * EventLog tags used when logging into the event log. Note the values must be sync with
+     * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct
+     * name translation.
+      */
+    private static final int LOG_TAG_NETSTATS_MOBILE_SAMPLE = 51100;
+    private static final int LOG_TAG_NETSTATS_WIFI_SAMPLE = 51101;
+
+    // TODO: Replace the hardcoded string and move it into ConnectivitySettingsManager.
+    private static final String NETSTATS_COMBINE_SUBTYPE_ENABLED =
+            "netstats_combine_subtype_enabled";
+
+    // This is current path but may be changed soon.
+    private static final String UID_COUNTERSET_MAP_PATH =
+            "/sys/fs/bpf/map_netd_uid_counterset_map";
+    private static final String COOKIE_TAG_MAP_PATH =
+            "/sys/fs/bpf/map_netd_cookie_tag_map";
+    private static final String APP_UID_STATS_MAP_PATH =
+            "/sys/fs/bpf/map_netd_app_uid_stats_map";
+    private static final String STATS_MAP_A_PATH =
+            "/sys/fs/bpf/map_netd_stats_map_A";
+    private static final String STATS_MAP_B_PATH =
+            "/sys/fs/bpf/map_netd_stats_map_B";
+
+    private final Context mContext;
+    private final NetworkStatsFactory mStatsFactory;
+    private final AlarmManager mAlarmManager;
+    private final Clock mClock;
+    private final NetworkStatsSettings mSettings;
+    private final NetworkStatsObservers mStatsObservers;
+
+    private final File mSystemDir;
+    private final File mBaseDir;
+
+    private final PowerManager.WakeLock mWakeLock;
+
+    private final ContentObserver mContentObserver;
+    private final ContentResolver mContentResolver;
+
+    protected INetd mNetd;
+    private final AlertObserver mAlertObserver = new AlertObserver();
+
+    @VisibleForTesting
+    public static final String ACTION_NETWORK_STATS_POLL =
+            "com.android.server.action.NETWORK_STATS_POLL";
+    public static final String ACTION_NETWORK_STATS_UPDATED =
+            "com.android.server.action.NETWORK_STATS_UPDATED";
+
+    private PendingIntent mPollIntent;
+
+    /**
+     * Settings that can be changed externally.
+     */
+    public interface NetworkStatsSettings {
+        long getPollInterval();
+        long getPollDelay();
+        boolean getSampleEnabled();
+        boolean getAugmentEnabled();
+        /**
+         * When enabled, all mobile data is reported under {@link NetworkTemplate#NETWORK_TYPE_ALL}.
+         * When disabled, mobile data is broken down by a granular ratType representative of the
+         * actual ratType. {@see android.app.usage.NetworkStatsManager#getCollapsedRatType}.
+         * Enabling this decreases the level of detail but saves performance, disk space and
+         * amount of data logged.
+         */
+        boolean getCombineSubtypeEnabled();
+
+        class Config {
+            public final long bucketDuration;
+            public final long rotateAgeMillis;
+            public final long deleteAgeMillis;
+
+            public Config(long bucketDuration, long rotateAgeMillis, long deleteAgeMillis) {
+                this.bucketDuration = bucketDuration;
+                this.rotateAgeMillis = rotateAgeMillis;
+                this.deleteAgeMillis = deleteAgeMillis;
+            }
+        }
+
+        Config getDevConfig();
+        Config getXtConfig();
+        Config getUidConfig();
+        Config getUidTagConfig();
+
+        long getGlobalAlertBytes(long def);
+        long getDevPersistBytes(long def);
+        long getXtPersistBytes(long def);
+        long getUidPersistBytes(long def);
+        long getUidTagPersistBytes(long def);
+    }
+
+    private final Object mStatsLock = new Object();
+
+    /** Set of currently active ifaces. */
+    @GuardedBy("mStatsLock")
+    private final ArrayMap<String, NetworkIdentitySet> mActiveIfaces = new ArrayMap<>();
+
+    /** Set of currently active ifaces for UID stats. */
+    @GuardedBy("mStatsLock")
+    private final ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces = new ArrayMap<>();
+
+    /** Current default active iface. */
+    @GuardedBy("mStatsLock")
+    private String mActiveIface;
+
+    /** Set of any ifaces associated with mobile networks since boot. */
+    private volatile String[] mMobileIfaces = new String[0];
+
+    /** Set of any ifaces associated with wifi networks since boot. */
+    private volatile String[] mWifiIfaces = new String[0];
+
+    /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */
+    @GuardedBy("mStatsLock")
+    private Network[] mDefaultNetworks = new Network[0];
+
+    /** Last states of all networks sent from ConnectivityService. */
+    @GuardedBy("mStatsLock")
+    @Nullable
+    private NetworkStateSnapshot[] mLastNetworkStateSnapshots = null;
+
+    private final DropBoxNonMonotonicObserver mNonMonotonicObserver =
+            new DropBoxNonMonotonicObserver();
+
+    private static final int MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS = 100;
+    private final CopyOnWriteArrayList<NetworkStatsProviderCallbackImpl> mStatsProviderCbList =
+            new CopyOnWriteArrayList<>();
+    /** Semaphore used to wait for stats provider to respond to request stats update. */
+    private final Semaphore mStatsProviderSem = new Semaphore(0, true);
+
+    @GuardedBy("mStatsLock")
+    private NetworkStatsRecorder mDevRecorder;
+    @GuardedBy("mStatsLock")
+    private NetworkStatsRecorder mXtRecorder;
+    @GuardedBy("mStatsLock")
+    private NetworkStatsRecorder mUidRecorder;
+    @GuardedBy("mStatsLock")
+    private NetworkStatsRecorder mUidTagRecorder;
+
+    /** Cached {@link #mXtRecorder} stats. */
+    @GuardedBy("mStatsLock")
+    private NetworkStatsCollection mXtStatsCached;
+
+    /**
+     * Current counter sets for each UID.
+     * TODO: maybe remove mActiveUidCounterSet and read UidCouneterSet value from mUidCounterSetMap
+     * directly ? But if mActiveUidCounterSet would be accessed very frequently, maybe keep
+     * mActiveUidCounterSet to avoid accessing kernel too frequently.
+     */
+    private SparseIntArray mActiveUidCounterSet = new SparseIntArray();
+    private final IBpfMap<U32, U8> mUidCounterSetMap;
+    private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap;
+    private final IBpfMap<StatsMapKey, StatsMapValue> mStatsMapA;
+    private final IBpfMap<StatsMapKey, StatsMapValue> mStatsMapB;
+    private final IBpfMap<UidStatsMapKey, StatsMapValue> mAppUidStatsMap;
+
+    /** Data layer operation counters for splicing into other structures. */
+    private NetworkStats mUidOperations = new NetworkStats(0L, 10);
+
+    @NonNull
+    private final Handler mHandler;
+
+    private volatile boolean mSystemReady;
+    private long mPersistThreshold = 2 * MB_IN_BYTES;
+    private long mGlobalAlertBytes;
+
+    private static final long POLL_RATE_LIMIT_MS = 15_000;
+
+    private long mLastStatsSessionPoll;
+
+    /** Map from UID to number of opened sessions */
+    @GuardedBy("mOpenSessionCallsPerUid")
+    private final SparseIntArray mOpenSessionCallsPerUid = new SparseIntArray();
+
+    private final static int DUMP_STATS_SESSION_COUNT = 20;
+
+    @NonNull
+    private final Dependencies mDeps;
+
+    @NonNull
+    private final NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
+
+    @NonNull
+    private final LocationPermissionChecker mLocationPermissionChecker;
+
+    @NonNull
+    private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
+
+    private static @NonNull File getDefaultSystemDir() {
+        return new File(Environment.getDataDirectory(), "system");
+    }
+
+    private static @NonNull File getDefaultBaseDir() {
+        File baseDir = new File(getDefaultSystemDir(), "netstats");
+        baseDir.mkdirs();
+        return baseDir;
+    }
+
+    private static @NonNull Clock getDefaultClock() {
+        return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
+                Clock.systemUTC());
+    }
+
+    private final class NetworkStatsHandler extends Handler {
+        NetworkStatsHandler(@NonNull Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_PERFORM_POLL: {
+                    performPoll(FLAG_PERSIST_ALL);
+                    break;
+                }
+                case MSG_NOTIFY_NETWORK_STATUS: {
+                    // If no cached states, ignore.
+                    if (mLastNetworkStateSnapshots == null) break;
+                    // TODO (b/181642673): Protect mDefaultNetworks from concurrent accessing.
+                    handleNotifyNetworkStatus(
+                            mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface);
+                    break;
+                }
+                case MSG_PERFORM_POLL_REGISTER_ALERT: {
+                    performPoll(FLAG_PERSIST_NETWORK);
+                    registerGlobalAlert();
+                    break;
+                }
+                case MSG_BROADCAST_NETWORK_STATS_UPDATED: {
+                    final Intent updatedIntent = new Intent(ACTION_NETWORK_STATS_UPDATED);
+                    updatedIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+                    mContext.sendBroadcastAsUser(updatedIntent, UserHandle.ALL,
+                            READ_NETWORK_USAGE_HISTORY);
+                    break;
+                }
+            }
+        }
+    }
+
+    /** Creates a new NetworkStatsService */
+    public static NetworkStatsService create(Context context) {
+        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        PowerManager.WakeLock wakeLock =
+                powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+        final INetd netd = INetd.Stub.asInterface(
+                (IBinder) context.getSystemService(Context.NETD_SERVICE));
+        final NetworkStatsService service = new NetworkStatsService(context,
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
+                alarmManager, wakeLock, getDefaultClock(),
+                new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
+                new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(),
+                new Dependencies());
+
+        return service;
+    }
+
+    // This must not be called outside of tests, even within the same package, as this constructor
+    // does not register the local service. Use the create() helper above.
+    @VisibleForTesting
+    NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
+            PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
+            NetworkStatsFactory factory, NetworkStatsObservers statsObservers, File systemDir,
+            File baseDir, @NonNull Dependencies deps) {
+        mContext = Objects.requireNonNull(context, "missing Context");
+        mNetd = Objects.requireNonNull(netd, "missing Netd");
+        mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
+        mClock = Objects.requireNonNull(clock, "missing Clock");
+        mSettings = Objects.requireNonNull(settings, "missing NetworkStatsSettings");
+        mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
+        mStatsFactory = Objects.requireNonNull(factory, "missing factory");
+        mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
+        mSystemDir = Objects.requireNonNull(systemDir, "missing systemDir");
+        mBaseDir = Objects.requireNonNull(baseDir, "missing baseDir");
+        mDeps = Objects.requireNonNull(deps, "missing Dependencies");
+
+        final HandlerThread handlerThread = mDeps.makeHandlerThread();
+        handlerThread.start();
+        mHandler = new NetworkStatsHandler(handlerThread.getLooper());
+        mNetworkStatsSubscriptionsMonitor = deps.makeSubscriptionsMonitor(mContext,
+                (command) -> mHandler.post(command) , this);
+        mContentResolver = mContext.getContentResolver();
+        mContentObserver = mDeps.makeContentObserver(mHandler, mSettings,
+                mNetworkStatsSubscriptionsMonitor);
+        mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
+        mInterfaceMapUpdater = mDeps.makeBpfInterfaceMapUpdater(mContext, mHandler);
+        mInterfaceMapUpdater.start();
+        mUidCounterSetMap = mDeps.getUidCounterSetMap();
+        mCookieTagMap = mDeps.getCookieTagMap();
+        mStatsMapA = mDeps.getStatsMapA();
+        mStatsMapB = mDeps.getStatsMapB();
+        mAppUidStatsMap = mDeps.getAppUidStatsMap();
+    }
+
+    /**
+     * Dependencies of NetworkStatsService, for injection in tests.
+     */
+    // TODO: Move more stuff into dependencies object.
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * Create a HandlerThread to use in NetworkStatsService.
+         */
+        @NonNull
+        public HandlerThread makeHandlerThread() {
+            return new HandlerThread(TAG);
+        }
+
+        /**
+         * Create a {@link NetworkStatsSubscriptionsMonitor}, can be used to monitor RAT change
+         * event in NetworkStatsService.
+         */
+        @NonNull
+        public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(@NonNull Context context,
+                @NonNull Executor executor, @NonNull NetworkStatsService service) {
+            // TODO: Update RatType passively in NSS, instead of querying into the monitor
+            //  when notifyNetworkStatus.
+            return new NetworkStatsSubscriptionsMonitor(context, executor,
+                    (subscriberId, type) -> service.handleOnCollapsedRatTypeChanged());
+        }
+
+        /**
+         * Create a ContentObserver instance which is used to observe settings changes,
+         * and dispatch onChange events on handler thread.
+         */
+        public @NonNull ContentObserver makeContentObserver(@NonNull Handler handler,
+                @NonNull NetworkStatsSettings settings,
+                @NonNull NetworkStatsSubscriptionsMonitor monitor) {
+            return new ContentObserver(handler) {
+                @Override
+                public void onChange(boolean selfChange, @NonNull Uri uri) {
+                    if (!settings.getCombineSubtypeEnabled()) {
+                        monitor.start();
+                    } else {
+                        monitor.stop();
+                    }
+                }
+            };
+        }
+
+        /**
+         * @see LocationPermissionChecker
+         */
+        public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+            return new LocationPermissionChecker(context);
+        }
+
+        /** Create BpfInterfaceMapUpdater to update bpf interface map. */
+        @NonNull
+        public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
+                @NonNull Context ctx, @NonNull Handler handler) {
+            return new BpfInterfaceMapUpdater(ctx, handler);
+        }
+
+        /** Get counter sets map for each UID. */
+        public IBpfMap<U32, U8> getUidCounterSetMap() {
+            try {
+                return new BpfMap<U32, U8>(UID_COUNTERSET_MAP_PATH, BpfMap.BPF_F_RDWR,
+                        U32.class, U8.class);
+            } catch (ErrnoException e) {
+                Log.wtf(TAG, "Cannot open uid counter set map: " + e);
+                return null;
+            }
+        }
+
+        /** Gets the cookie tag map */
+        public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
+            try {
+                return new BpfMap<CookieTagMapKey, CookieTagMapValue>(COOKIE_TAG_MAP_PATH,
+                        BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class);
+            } catch (ErrnoException e) {
+                Log.wtf(TAG, "Cannot open cookie tag map: " + e);
+                return null;
+            }
+        }
+
+        /** Gets stats map A */
+        public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
+            try {
+                return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_A_PATH,
+                        BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+            } catch (ErrnoException e) {
+                Log.wtf(TAG, "Cannot open stats map A: " + e);
+                return null;
+            }
+        }
+
+        /** Gets stats map B */
+        public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
+            try {
+                return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_B_PATH,
+                        BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+            } catch (ErrnoException e) {
+                Log.wtf(TAG, "Cannot open stats map B: " + e);
+                return null;
+            }
+        }
+
+        /** Gets the uid stats map */
+        public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
+            try {
+                return new BpfMap<UidStatsMapKey, StatsMapValue>(APP_UID_STATS_MAP_PATH,
+                        BpfMap.BPF_F_RDWR, UidStatsMapKey.class, StatsMapValue.class);
+            } catch (ErrnoException e) {
+                Log.wtf(TAG, "Cannot open app uid stats map: " + e);
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Observer that watches for {@link INetdUnsolicitedEventListener} alerts.
+     */
+    @VisibleForTesting
+    public class AlertObserver extends BaseNetdUnsolicitedEventListener {
+        @Override
+        public void onQuotaLimitReached(@NonNull String alertName, @NonNull String ifName) {
+            PermissionUtils.enforceNetworkStackPermission(mContext);
+
+            if (LIMIT_GLOBAL_ALERT.equals(alertName)) {
+                // kick off background poll to collect network stats unless there is already
+                // such a call pending; UID stats are handled during normal polling interval.
+                if (!mHandler.hasMessages(MSG_PERFORM_POLL_REGISTER_ALERT)) {
+                    mHandler.sendEmptyMessageDelayed(MSG_PERFORM_POLL_REGISTER_ALERT,
+                            mSettings.getPollDelay());
+                }
+            }
+        }
+    }
+
+    public void systemReady() {
+        synchronized (mStatsLock) {
+            mSystemReady = true;
+
+            // create data recorders along with historical rotators
+            mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false);
+            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false);
+            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false);
+            mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true);
+
+            updatePersistThresholdsLocked();
+
+            // upgrade any legacy stats, migrating them to rotated files
+            maybeUpgradeLegacyStatsLocked();
+
+            // read historical network stats from disk, since policy service
+            // might need them right away.
+            mXtStatsCached = mXtRecorder.getOrLoadCompleteLocked();
+
+            // bootstrap initial stats to prevent double-counting later
+            bootstrapStatsLocked();
+        }
+
+        // watch for tethering changes
+        final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class);
+        tetheringManager.registerTetheringEventCallback(
+                (command) -> mHandler.post(command), mTetherListener);
+
+        // listen for periodic polling events
+        final IntentFilter pollFilter = new IntentFilter(ACTION_NETWORK_STATS_POLL);
+        mContext.registerReceiver(mPollReceiver, pollFilter, READ_NETWORK_USAGE_HISTORY, mHandler);
+
+        // listen for uid removal to clean stats
+        final IntentFilter removedFilter = new IntentFilter(ACTION_UID_REMOVED);
+        mContext.registerReceiver(mRemovedReceiver, removedFilter, null, mHandler);
+
+        // listen for user changes to clean stats
+        final IntentFilter userFilter = new IntentFilter(ACTION_USER_REMOVED);
+        mContext.registerReceiver(mUserReceiver, userFilter, null, mHandler);
+
+        // persist stats during clean shutdown
+        final IntentFilter shutdownFilter = new IntentFilter(ACTION_SHUTDOWN);
+        mContext.registerReceiver(mShutdownReceiver, shutdownFilter);
+
+        try {
+            mNetd.registerUnsolicitedEventListener(mAlertObserver);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.wtf(TAG, "Error registering event listener :", e);
+        }
+
+        //  schedule periodic pall alarm based on {@link NetworkStatsSettings#getPollInterval()}.
+        final PendingIntent pollIntent =
+                PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NETWORK_STATS_POLL),
+                        PendingIntent.FLAG_IMMUTABLE);
+
+        final long currentRealtime = SystemClock.elapsedRealtime();
+        mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentRealtime,
+                mSettings.getPollInterval(), pollIntent);
+
+        mContentResolver.registerContentObserver(Settings.Global
+                .getUriFor(NETSTATS_COMBINE_SUBTYPE_ENABLED),
+                        false /* notifyForDescendants */, mContentObserver);
+
+        // Post a runnable on handler thread to call onChange(). It's for getting current value of
+        // NETSTATS_COMBINE_SUBTYPE_ENABLED to decide start or stop monitoring RAT type changes.
+        mHandler.post(() -> mContentObserver.onChange(false, Settings.Global
+                .getUriFor(NETSTATS_COMBINE_SUBTYPE_ENABLED)));
+
+        registerGlobalAlert();
+    }
+
+    private NetworkStatsRecorder buildRecorder(
+            String prefix, NetworkStatsSettings.Config config, boolean includeTags) {
+        final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
+                Context.DROPBOX_SERVICE);
+        return new NetworkStatsRecorder(new FileRotator(
+                mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+                mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
+    }
+
+    @GuardedBy("mStatsLock")
+    private void shutdownLocked() {
+        final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class);
+        tetheringManager.unregisterTetheringEventCallback(mTetherListener);
+        mContext.unregisterReceiver(mPollReceiver);
+        mContext.unregisterReceiver(mRemovedReceiver);
+        mContext.unregisterReceiver(mUserReceiver);
+        mContext.unregisterReceiver(mShutdownReceiver);
+
+        if (!mSettings.getCombineSubtypeEnabled()) {
+            mNetworkStatsSubscriptionsMonitor.stop();
+        }
+
+        mContentResolver.unregisterContentObserver(mContentObserver);
+
+        final long currentTime = mClock.millis();
+
+        // persist any pending stats
+        mDevRecorder.forcePersistLocked(currentTime);
+        mXtRecorder.forcePersistLocked(currentTime);
+        mUidRecorder.forcePersistLocked(currentTime);
+        mUidTagRecorder.forcePersistLocked(currentTime);
+
+        mSystemReady = false;
+    }
+
+    @GuardedBy("mStatsLock")
+    private void maybeUpgradeLegacyStatsLocked() {
+        File file;
+        try {
+            file = new File(mSystemDir, "netstats.bin");
+            if (file.exists()) {
+                mDevRecorder.importLegacyNetworkLocked(file);
+                file.delete();
+            }
+
+            file = new File(mSystemDir, "netstats_xt.bin");
+            if (file.exists()) {
+                file.delete();
+            }
+
+            file = new File(mSystemDir, "netstats_uid.bin");
+            if (file.exists()) {
+                mUidRecorder.importLegacyUidLocked(file);
+                mUidTagRecorder.importLegacyUidLocked(file);
+                file.delete();
+            }
+        } catch (IOException e) {
+            Log.wtf(TAG, "problem during legacy upgrade", e);
+        } catch (OutOfMemoryError e) {
+            Log.wtf(TAG, "problem during legacy upgrade", e);
+        }
+    }
+
+    /**
+     * Register for a global alert that is delivered through {@link AlertObserver}
+     * or {@link NetworkStatsProviderCallback#onAlertReached()} once a threshold amount of data has
+     * been transferred.
+     */
+    private void registerGlobalAlert() {
+        try {
+            mNetd.bandwidthSetGlobalAlert(mGlobalAlertBytes);
+        } catch (IllegalStateException e) {
+            Log.w(TAG, "problem registering for global alert: " + e);
+        } catch (RemoteException e) {
+            // ignored; service lives in system_server
+        }
+        invokeForAllStatsProviderCallbacks((cb) -> cb.mProvider.onSetAlert(mGlobalAlertBytes));
+    }
+
+    @Override
+    public INetworkStatsSession openSession() {
+        return openSessionInternal(NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN, null);
+    }
+
+    @Override
+    public INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage) {
+        return openSessionInternal(flags, callingPackage);
+    }
+
+    private boolean isRateLimitedForPoll(int callingUid) {
+        if (callingUid == android.os.Process.SYSTEM_UID) {
+            return false;
+        }
+
+        final long lastCallTime;
+        final long now = SystemClock.elapsedRealtime();
+        synchronized (mOpenSessionCallsPerUid) {
+            int calls = mOpenSessionCallsPerUid.get(callingUid, 0);
+            mOpenSessionCallsPerUid.put(callingUid, calls + 1);
+            lastCallTime = mLastStatsSessionPoll;
+            mLastStatsSessionPoll = now;
+        }
+
+        return now - lastCallTime < POLL_RATE_LIMIT_MS;
+    }
+
+    private int restrictFlagsForCaller(int flags) {
+        // All non-privileged callers are not allowed to turn off POLL_ON_OPEN.
+        final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                android.Manifest.permission.NETWORK_STACK);
+        if (!isPrivileged) {
+            flags |= NetworkStatsManager.FLAG_POLL_ON_OPEN;
+        }
+        // Non-system uids are rate limited for POLL_ON_OPEN.
+        final int callingUid = Binder.getCallingUid();
+        flags = isRateLimitedForPoll(callingUid)
+                ? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN)
+                : flags;
+        return flags;
+    }
+
+    private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
+        final int restrictedFlags = restrictFlagsForCaller(flags);
+        if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
+                | NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                performPoll(FLAG_PERSIST_ALL);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        // return an IBinder which holds strong references to any loaded stats
+        // for its lifetime; when caller closes only weak references remain.
+
+        return new INetworkStatsSession.Stub() {
+            private final int mCallingUid = Binder.getCallingUid();
+            private final String mCallingPackage = callingPackage;
+            private final @NetworkStatsAccess.Level int mAccessLevel = checkAccessLevel(
+                    callingPackage);
+
+            private NetworkStatsCollection mUidComplete;
+            private NetworkStatsCollection mUidTagComplete;
+
+            private NetworkStatsCollection getUidComplete() {
+                synchronized (mStatsLock) {
+                    if (mUidComplete == null) {
+                        mUidComplete = mUidRecorder.getOrLoadCompleteLocked();
+                    }
+                    return mUidComplete;
+                }
+            }
+
+            private NetworkStatsCollection getUidTagComplete() {
+                synchronized (mStatsLock) {
+                    if (mUidTagComplete == null) {
+                        mUidTagComplete = mUidTagRecorder.getOrLoadCompleteLocked();
+                    }
+                    return mUidTagComplete;
+                }
+            }
+
+            @Override
+            public int[] getRelevantUids() {
+                return getUidComplete().getRelevantUids(mAccessLevel);
+            }
+
+            @Override
+            public NetworkStats getDeviceSummaryForNetwork(
+                    NetworkTemplate template, long start, long end) {
+                enforceTemplatePermissions(template, callingPackage);
+                return internalGetSummaryForNetwork(template, restrictedFlags, start, end,
+                        mAccessLevel, mCallingUid);
+            }
+
+            @Override
+            public NetworkStats getSummaryForNetwork(
+                    NetworkTemplate template, long start, long end) {
+                enforceTemplatePermissions(template, callingPackage);
+                return internalGetSummaryForNetwork(template, restrictedFlags, start, end,
+                        mAccessLevel, mCallingUid);
+            }
+
+            // TODO: Remove this after all callers are removed.
+            @Override
+            public NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields) {
+                enforceTemplatePermissions(template, callingPackage);
+                return internalGetHistoryForNetwork(template, restrictedFlags, fields,
+                        mAccessLevel, mCallingUid, Long.MIN_VALUE, Long.MAX_VALUE);
+            }
+
+            @Override
+            public NetworkStatsHistory getHistoryIntervalForNetwork(NetworkTemplate template,
+                    int fields, long start, long end) {
+                enforceTemplatePermissions(template, callingPackage);
+                // TODO(b/200768422): Redact returned history if the template is location
+                //  sensitive but the caller is not privileged.
+                return internalGetHistoryForNetwork(template, restrictedFlags, fields,
+                        mAccessLevel, mCallingUid, start, end);
+            }
+
+            @Override
+            public NetworkStats getSummaryForAllUid(
+                    NetworkTemplate template, long start, long end, boolean includeTags) {
+                enforceTemplatePermissions(template, callingPackage);
+                try {
+                    final NetworkStats stats = getUidComplete()
+                            .getSummary(template, start, end, mAccessLevel, mCallingUid);
+                    if (includeTags) {
+                        final NetworkStats tagStats = getUidTagComplete()
+                                .getSummary(template, start, end, mAccessLevel, mCallingUid);
+                        stats.combineAllValues(tagStats);
+                    }
+                    return stats;
+                } catch (NullPointerException e) {
+                    throw e;
+                }
+            }
+
+            @Override
+            public NetworkStats getTaggedSummaryForAllUid(
+                    NetworkTemplate template, long start, long end) {
+                enforceTemplatePermissions(template, callingPackage);
+                try {
+                    final NetworkStats tagStats = getUidTagComplete()
+                            .getSummary(template, start, end, mAccessLevel, mCallingUid);
+                    return tagStats;
+                } catch (NullPointerException e) {
+                    throw e;
+                }
+            }
+
+            @Override
+            public NetworkStatsHistory getHistoryForUid(
+                    NetworkTemplate template, int uid, int set, int tag, int fields) {
+                enforceTemplatePermissions(template, callingPackage);
+                // NOTE: We don't augment UID-level statistics
+                if (tag == TAG_NONE) {
+                    return getUidComplete().getHistory(template, null, uid, set, tag, fields,
+                            Long.MIN_VALUE, Long.MAX_VALUE, mAccessLevel, mCallingUid);
+                } else {
+                    return getUidTagComplete().getHistory(template, null, uid, set, tag, fields,
+                            Long.MIN_VALUE, Long.MAX_VALUE, mAccessLevel, mCallingUid);
+                }
+            }
+
+            @Override
+            public NetworkStatsHistory getHistoryIntervalForUid(
+                    NetworkTemplate template, int uid, int set, int tag, int fields,
+                    long start, long end) {
+                enforceTemplatePermissions(template, callingPackage);
+                // TODO(b/200768422): Redact returned history if the template is location
+                //  sensitive but the caller is not privileged.
+                // NOTE: We don't augment UID-level statistics
+                if (tag == TAG_NONE) {
+                    return getUidComplete().getHistory(template, null, uid, set, tag, fields,
+                            start, end, mAccessLevel, mCallingUid);
+                } else if (uid == Binder.getCallingUid()) {
+                    return getUidTagComplete().getHistory(template, null, uid, set, tag, fields,
+                            start, end, mAccessLevel, mCallingUid);
+                } else {
+                    throw new SecurityException("Calling package " + mCallingPackage
+                            + " cannot access tag information from a different uid");
+                }
+            }
+
+            @Override
+            public void close() {
+                mUidComplete = null;
+                mUidTagComplete = null;
+            }
+        };
+    }
+
+    private void enforceTemplatePermissions(@NonNull NetworkTemplate template,
+            @NonNull String callingPackage) {
+        // For a template with wifi network keys, it is possible for a malicious
+        // client to track the user locations via querying data usage. Thus, enforce
+        // fine location permission check.
+        if (!template.getWifiNetworkKeys().isEmpty()) {
+            final boolean canAccessFineLocation = mLocationPermissionChecker
+                    .checkCallersLocationPermission(callingPackage,
+                    null /* featureId */,
+                            Binder.getCallingUid(),
+                            false /* coarseForTargetSdkLessThanQ */,
+                            null /* message */);
+            if (!canAccessFineLocation) {
+                throw new SecurityException("Access fine location is required when querying"
+                        + " with wifi network keys, make sure the app has the necessary"
+                        + "permissions and the location toggle is on.");
+            }
+        }
+    }
+
+    private @NetworkStatsAccess.Level int checkAccessLevel(String callingPackage) {
+        return NetworkStatsAccess.checkAccessLevel(
+                mContext, Binder.getCallingPid(), Binder.getCallingUid(), callingPackage);
+    }
+
+    /**
+     * Find the most relevant {@link SubscriptionPlan} for the given
+     * {@link NetworkTemplate} and flags. This is typically used to augment
+     * local measurement results to match a known anchor from the carrier.
+     */
+    private SubscriptionPlan resolveSubscriptionPlan(NetworkTemplate template, int flags) {
+        SubscriptionPlan plan = null;
+        if ((flags & NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN) != 0
+                && mSettings.getAugmentEnabled()) {
+            if (LOGD) Log.d(TAG, "Resolving plan for " + template);
+            final long token = Binder.clearCallingIdentity();
+            try {
+                plan = mContext.getSystemService(NetworkPolicyManager.class)
+                        .getSubscriptionPlan(template);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+            if (LOGD) Log.d(TAG, "Resolved to plan " + plan);
+        }
+        return plan;
+    }
+
+    /**
+     * Return network summary, splicing between DEV and XT stats when
+     * appropriate.
+     */
+    private NetworkStats internalGetSummaryForNetwork(NetworkTemplate template, int flags,
+            long start, long end, @NetworkStatsAccess.Level int accessLevel, int callingUid) {
+        // We've been using pure XT stats long enough that we no longer need to
+        // splice DEV and XT together.
+        final NetworkStatsHistory history = internalGetHistoryForNetwork(template, flags, FIELD_ALL,
+                accessLevel, callingUid, start, end);
+
+        final long now = System.currentTimeMillis();
+        final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null);
+
+        final NetworkStats stats = new NetworkStats(end - start, 1);
+        stats.insertEntry(new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE,
+                METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, entry.rxBytes, entry.rxPackets,
+                entry.txBytes, entry.txPackets, entry.operations));
+        return stats;
+    }
+
+    /**
+     * Return network history, splicing between DEV and XT stats when
+     * appropriate.
+     */
+    private NetworkStatsHistory internalGetHistoryForNetwork(NetworkTemplate template,
+            int flags, int fields, @NetworkStatsAccess.Level int accessLevel, int callingUid,
+            long start, long end) {
+        // We've been using pure XT stats long enough that we no longer need to
+        // splice DEV and XT together.
+        final SubscriptionPlan augmentPlan = resolveSubscriptionPlan(template, flags);
+        synchronized (mStatsLock) {
+            return mXtStatsCached.getHistory(template, augmentPlan,
+                    UID_ALL, SET_ALL, TAG_NONE, fields, start, end, accessLevel, callingUid);
+        }
+    }
+
+    private long getNetworkTotalBytes(NetworkTemplate template, long start, long end) {
+        assertSystemReady();
+
+        return internalGetSummaryForNetwork(template,
+                NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN, start, end,
+                NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotalBytes();
+    }
+
+    private NetworkStats getNetworkUidBytes(NetworkTemplate template, long start, long end) {
+        assertSystemReady();
+
+        final NetworkStatsCollection uidComplete;
+        synchronized (mStatsLock) {
+            uidComplete = mUidRecorder.getOrLoadCompleteLocked();
+        }
+        return uidComplete.getSummary(template, start, end, NetworkStatsAccess.Level.DEVICE,
+                android.os.Process.SYSTEM_UID);
+    }
+
+    @Override
+    public NetworkStats getDataLayerSnapshotForUid(int uid) throws RemoteException {
+        if (Binder.getCallingUid() != uid) {
+            Log.w(TAG, "Snapshots only available for calling UID");
+            return new NetworkStats(SystemClock.elapsedRealtime(), 0);
+        }
+
+        // TODO: switch to data layer stats once kernel exports
+        // for now, read network layer stats and flatten across all ifaces.
+        // This function is used to query NeworkStats for calle's uid. The only caller method
+        // TrafficStats#getDataLayerSnapshotForUid alrady claim no special permission to query
+        // its own NetworkStats.
+        final long ident = Binder.clearCallingIdentity();
+        final NetworkStats networkLayer;
+        try {
+            networkLayer = readNetworkStatsUidDetail(uid, INTERFACES_ALL, TAG_ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+
+        // splice in operation counts
+        networkLayer.spliceOperationsFrom(mUidOperations);
+
+        final NetworkStats dataLayer = new NetworkStats(
+                networkLayer.getElapsedRealtime(), networkLayer.size());
+
+        NetworkStats.Entry entry = null;
+        for (int i = 0; i < networkLayer.size(); i++) {
+            entry = networkLayer.getValues(i, entry);
+            entry.iface = IFACE_ALL;
+            dataLayer.combineValues(entry);
+        }
+
+        return dataLayer;
+    }
+
+    @Override
+    public NetworkStats getUidStatsForTransport(int transport) {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        try {
+            final String[] relevantIfaces =
+                    transport == TRANSPORT_WIFI ? mWifiIfaces : mMobileIfaces;
+            // TODO(b/215633405) : mMobileIfaces and mWifiIfaces already contain the stacked
+            // interfaces, so this is not useful, remove it.
+            final String[] ifacesToQuery =
+                    mStatsFactory.augmentWithStackedInterfaces(relevantIfaces);
+            return getNetworkStatsUidDetail(ifacesToQuery);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error compiling UID stats", e);
+            return new NetworkStats(0L, 0);
+        }
+    }
+
+    @Override
+    public String[] getMobileIfaces() {
+        // TODO (b/192758557): Remove debug log.
+        if (CollectionUtils.contains(mMobileIfaces, null)) {
+            throw new NullPointerException(
+                    "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
+        }
+        return mMobileIfaces.clone();
+    }
+
+    @Override
+    public void incrementOperationCount(int uid, int tag, int operationCount) {
+        if (Binder.getCallingUid() != uid) {
+            mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG);
+        }
+
+        if (operationCount < 0) {
+            throw new IllegalArgumentException("operation count can only be incremented");
+        }
+        if (tag == TAG_NONE) {
+            throw new IllegalArgumentException("operation count must have specific tag");
+        }
+
+        synchronized (mStatsLock) {
+            final int set = mActiveUidCounterSet.get(uid, SET_DEFAULT);
+            mUidOperations.combineValues(
+                    mActiveIface, uid, set, tag, 0L, 0L, 0L, 0L, operationCount);
+            mUidOperations.combineValues(
+                    mActiveIface, uid, set, TAG_NONE, 0L, 0L, 0L, 0L, operationCount);
+        }
+    }
+
+    private void setKernelCounterSet(int uid, int set) {
+        if (mUidCounterSetMap == null) {
+            Log.wtf(TAG, "Fail to set UidCounterSet: Null bpf map");
+            return;
+        }
+
+        if (set == SET_DEFAULT) {
+            try {
+                mUidCounterSetMap.deleteEntry(new U32(uid));
+            } catch (ErrnoException e) {
+                Log.w(TAG, "UidCounterSetMap.deleteEntry(" + uid + ") failed with errno: " + e);
+            }
+            return;
+        }
+
+        try {
+            mUidCounterSetMap.updateEntry(new U32(uid), new U8((short) set));
+        } catch (ErrnoException e) {
+            Log.w(TAG, "UidCounterSetMap.updateEntry(" + uid + ", " + set
+                    + ") failed with errno: " + e);
+        }
+    }
+
+    @VisibleForTesting
+    public void noteUidForeground(int uid, boolean uidForeground) {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        synchronized (mStatsLock) {
+            final int set = uidForeground ? SET_FOREGROUND : SET_DEFAULT;
+            final int oldSet = mActiveUidCounterSet.get(uid, SET_DEFAULT);
+            if (oldSet != set) {
+                mActiveUidCounterSet.put(uid, set);
+                setKernelCounterSet(uid, set);
+            }
+        }
+    }
+
+    /**
+     * Notify {@code NetworkStatsService} about network status changed.
+     */
+    public void notifyNetworkStatus(
+            @NonNull Network[] defaultNetworks,
+            @NonNull NetworkStateSnapshot[] networkStates,
+            @Nullable String activeIface,
+            @NonNull UnderlyingNetworkInfo[] underlyingNetworkInfos) {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        // Update the VPN underlying interfaces only after the poll is made and tun data has been
+        // migrated. Otherwise the migration would use the new interfaces instead of the ones that
+        // were current when the polled data was transferred.
+        mStatsFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+    }
+
+    @Override
+    public void forceUpdate() {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            performPoll(FLAG_PERSIST_ALL);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /** Advise persistence threshold; may be overridden internally. */
+    public void advisePersistThreshold(long thresholdBytes) {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        // clamp threshold into safe range
+        mPersistThreshold = NetworkStatsUtils.constrain(thresholdBytes,
+                128 * KB_IN_BYTES, 2 * MB_IN_BYTES);
+        if (LOGV) {
+            Log.v(TAG, "advisePersistThreshold() given " + thresholdBytes + ", clamped to "
+                    + mPersistThreshold);
+        }
+
+        final long oldGlobalAlertBytes = mGlobalAlertBytes;
+
+        // update and persist if beyond new thresholds
+        final long currentTime = mClock.millis();
+        synchronized (mStatsLock) {
+            if (!mSystemReady) return;
+
+            updatePersistThresholdsLocked();
+
+            mDevRecorder.maybePersistLocked(currentTime);
+            mXtRecorder.maybePersistLocked(currentTime);
+            mUidRecorder.maybePersistLocked(currentTime);
+            mUidTagRecorder.maybePersistLocked(currentTime);
+        }
+
+        if (oldGlobalAlertBytes != mGlobalAlertBytes) {
+            registerGlobalAlert();
+        }
+    }
+
+    @Override
+    public DataUsageRequest registerUsageCallback(@NonNull String callingPackage,
+                @NonNull DataUsageRequest request, @NonNull IUsageCallback callback) {
+        Objects.requireNonNull(callingPackage, "calling package is null");
+        Objects.requireNonNull(request, "DataUsageRequest is null");
+        Objects.requireNonNull(request.template, "NetworkTemplate is null");
+        Objects.requireNonNull(callback, "callback is null");
+
+        int callingUid = Binder.getCallingUid();
+        @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
+        DataUsageRequest normalizedRequest;
+        final long token = Binder.clearCallingIdentity();
+        try {
+            normalizedRequest = mStatsObservers.register(mContext,
+                    request, callback, callingUid, accessLevel);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        // Create baseline stats
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL));
+
+        return normalizedRequest;
+   }
+
+    @Override
+    public void unregisterUsageRequest(DataUsageRequest request) {
+        Objects.requireNonNull(request, "DataUsageRequest is null");
+
+        int callingUid = Binder.getCallingUid();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mStatsObservers.unregister(request, callingUid);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public long getUidStats(int uid, int type) {
+        final int callingUid = Binder.getCallingUid();
+        if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
+            return UNSUPPORTED;
+        }
+        return nativeGetUidStat(uid, type);
+    }
+
+    @Override
+    public long getIfaceStats(@NonNull String iface, int type) {
+        Objects.requireNonNull(iface);
+        long nativeIfaceStats = nativeGetIfaceStat(iface, type);
+        if (nativeIfaceStats == -1) {
+            return nativeIfaceStats;
+        } else {
+            // When tethering offload is in use, nativeIfaceStats does not contain usage from
+            // offload, add it back here. Note that the included statistics might be stale
+            // since polling newest stats from hardware might impact system health and not
+            // suitable for TrafficStats API use cases.
+            return nativeIfaceStats + getProviderIfaceStats(iface, type);
+        }
+    }
+
+    @Override
+    public long getTotalStats(int type) {
+        long nativeTotalStats = nativeGetTotalStat(type);
+        if (nativeTotalStats == -1) {
+            return nativeTotalStats;
+        } else {
+            // Refer to comment in getIfaceStats
+            return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type);
+        }
+    }
+
+    private long getProviderIfaceStats(@Nullable String iface, int type) {
+        final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
+        final HashSet<String> limitIfaces;
+        if (iface == IFACE_ALL) {
+            limitIfaces = null;
+        } else {
+            limitIfaces = new HashSet<>();
+            limitIfaces.add(iface);
+        }
+        final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces);
+        switch (type) {
+            case TrafficStats.TYPE_RX_BYTES:
+                return entry.rxBytes;
+            case TrafficStats.TYPE_RX_PACKETS:
+                return entry.rxPackets;
+            case TrafficStats.TYPE_TX_BYTES:
+                return entry.txBytes;
+            case TrafficStats.TYPE_TX_PACKETS:
+                return entry.txPackets;
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * Update {@link NetworkStatsRecorder} and {@link #mGlobalAlertBytes} to
+     * reflect current {@link #mPersistThreshold} value. Always defers to
+     * {@link Global} values when defined.
+     */
+    @GuardedBy("mStatsLock")
+    private void updatePersistThresholdsLocked() {
+        mDevRecorder.setPersistThreshold(mSettings.getDevPersistBytes(mPersistThreshold));
+        mXtRecorder.setPersistThreshold(mSettings.getXtPersistBytes(mPersistThreshold));
+        mUidRecorder.setPersistThreshold(mSettings.getUidPersistBytes(mPersistThreshold));
+        mUidTagRecorder.setPersistThreshold(mSettings.getUidTagPersistBytes(mPersistThreshold));
+        mGlobalAlertBytes = mSettings.getGlobalAlertBytes(mPersistThreshold);
+    }
+
+    /**
+     * Listener that watches for {@link TetheringManager} to claim interface pairs.
+     */
+    private final TetheringManager.TetheringEventCallback mTetherListener =
+            new TetheringManager.TetheringEventCallback() {
+                @Override
+                public void onUpstreamChanged(@Nullable Network network) {
+                    performPoll(FLAG_PERSIST_NETWORK);
+                }
+            };
+
+    private BroadcastReceiver mPollReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // on background handler thread, and verified UPDATE_DEVICE_STATS
+            // permission above.
+            performPoll(FLAG_PERSIST_ALL);
+
+            // verify that we're watching global alert
+            registerGlobalAlert();
+        }
+    };
+
+    private BroadcastReceiver mRemovedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // on background handler thread, and UID_REMOVED is protected
+            // broadcast.
+
+            final int uid = intent.getIntExtra(EXTRA_UID, -1);
+            if (uid == -1) return;
+
+            synchronized (mStatsLock) {
+                mWakeLock.acquire();
+                try {
+                    removeUidsLocked(uid);
+                } finally {
+                    mWakeLock.release();
+                }
+            }
+        }
+    };
+
+    private BroadcastReceiver mUserReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // On background handler thread, and USER_REMOVED is protected
+            // broadcast.
+
+            final UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
+            if (userHandle == null) return;
+
+            synchronized (mStatsLock) {
+                mWakeLock.acquire();
+                try {
+                    removeUserLocked(userHandle);
+                } finally {
+                    mWakeLock.release();
+                }
+            }
+        }
+    };
+
+    private BroadcastReceiver mShutdownReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // SHUTDOWN is protected broadcast.
+            synchronized (mStatsLock) {
+                shutdownLocked();
+            }
+        }
+    };
+
+    /**
+     * Handle collapsed RAT type changed event.
+     */
+    @VisibleForTesting
+    public void handleOnCollapsedRatTypeChanged() {
+        // Protect service from frequently updating. Remove pending messages if any.
+        mHandler.removeMessages(MSG_NOTIFY_NETWORK_STATUS);
+        mHandler.sendMessageDelayed(
+                mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS), mSettings.getPollDelay());
+    }
+
+    private void handleNotifyNetworkStatus(
+            Network[] defaultNetworks,
+            NetworkStateSnapshot[] snapshots,
+            String activeIface) {
+        synchronized (mStatsLock) {
+            mWakeLock.acquire();
+            try {
+                mActiveIface = activeIface;
+                handleNotifyNetworkStatusLocked(defaultNetworks, snapshots);
+            } finally {
+                mWakeLock.release();
+            }
+        }
+    }
+
+    /**
+     * Inspect all current {@link NetworkStateSnapshot}s to derive mapping from {@code iface} to
+     * {@link NetworkStatsHistory}. When multiple networks are active on a single {@code iface},
+     * they are combined under a single {@link NetworkIdentitySet}.
+     */
+    @GuardedBy("mStatsLock")
+    private void handleNotifyNetworkStatusLocked(@NonNull Network[] defaultNetworks,
+            @NonNull NetworkStateSnapshot[] snapshots) {
+        if (!mSystemReady) return;
+        if (LOGV) Log.v(TAG, "handleNotifyNetworkStatusLocked()");
+
+        // take one last stats snapshot before updating iface mapping. this
+        // isn't perfect, since the kernel may already be counting traffic from
+        // the updated network.
+
+        // poll, but only persist network stats to keep codepath fast. UID stats
+        // will be persisted during next alarm poll event.
+        performPollLocked(FLAG_PERSIST_NETWORK);
+
+        // Rebuild active interfaces based on connected networks
+        mActiveIfaces.clear();
+        mActiveUidIfaces.clear();
+        // Update the list of default networks.
+        mDefaultNetworks = defaultNetworks;
+
+        mLastNetworkStateSnapshots = snapshots;
+
+        final boolean combineSubtypeEnabled = mSettings.getCombineSubtypeEnabled();
+        final ArraySet<String> mobileIfaces = new ArraySet<>();
+        final ArraySet<String> wifiIfaces = new ArraySet<>();
+        for (NetworkStateSnapshot snapshot : snapshots) {
+            final int displayTransport =
+                    getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes());
+            final boolean isMobile = (NetworkCapabilities.TRANSPORT_CELLULAR == displayTransport);
+            final boolean isWifi = (NetworkCapabilities.TRANSPORT_WIFI == displayTransport);
+            final boolean isDefault = CollectionUtils.contains(
+                    mDefaultNetworks, snapshot.getNetwork());
+            final int ratType = combineSubtypeEnabled ? NetworkTemplate.NETWORK_TYPE_ALL
+                    : getRatTypeForStateSnapshot(snapshot);
+            final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, snapshot,
+                    isDefault, ratType);
+
+            // Traffic occurring on the base interface is always counted for
+            // both total usage and UID details.
+            final String baseIface = snapshot.getLinkProperties().getInterfaceName();
+            if (baseIface != null) {
+                findOrCreateNetworkIdentitySet(mActiveIfaces, baseIface).add(ident);
+                findOrCreateNetworkIdentitySet(mActiveUidIfaces, baseIface).add(ident);
+
+                // Build a separate virtual interface for VT (Video Telephony) data usage.
+                // Only do this when IMS is not metered, but VT is metered.
+                // If IMS is metered, then the IMS network usage has already included VT usage.
+                // VT is considered always metered in framework's layer. If VT is not metered
+                // per carrier's policy, modem will report 0 usage for VT calls.
+                if (snapshot.getNetworkCapabilities().hasCapability(
+                        NetworkCapabilities.NET_CAPABILITY_IMS) && !ident.isMetered()) {
+
+                    // Copy the identify from IMS one but mark it as metered.
+                    NetworkIdentity vtIdent = new NetworkIdentity.Builder()
+                            .setType(ident.getType())
+                            .setRatType(ident.getRatType())
+                            .setSubscriberId(ident.getSubscriberId())
+                            .setWifiNetworkKey(ident.getWifiNetworkKey())
+                            .setRoaming(ident.isRoaming()).setMetered(true)
+                            .setDefaultNetwork(true)
+                            .setOemManaged(ident.getOemManaged())
+                            .setSubId(ident.getSubId()).build();
+                    final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot);
+                    findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent);
+                    findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent);
+                }
+
+                if (isMobile) {
+                    mobileIfaces.add(baseIface);
+                }
+                if (isWifi) {
+                    wifiIfaces.add(baseIface);
+                }
+            }
+
+            // Traffic occurring on stacked interfaces is usually clatd.
+            //
+            // UID stats are always counted on the stacked interface and never on the base
+            // interface, because the packets on the base interface do not actually match
+            // application sockets (they're not IPv4) and thus the app uid is not known.
+            // For receive this is obvious: packets must be translated from IPv6 to IPv4
+            // before the application socket can be found.
+            // For transmit: either they go through the clat daemon which by virtue of going
+            // through userspace strips the original socket association during the IPv4 to
+            // IPv6 translation process, or they are offloaded by eBPF, which doesn't:
+            // However, on an ebpf device the accounting is done in cgroup ebpf hooks,
+            // which don't trigger again post ebpf translation.
+            // (as such stats accounted to the clat uid are ignored)
+            //
+            // Interface stats are more complicated.
+            //
+            // eBPF offloaded 464xlat'ed packets never hit base interface ip6tables, and thus
+            // *all* statistics are collected by iptables on the stacked v4-* interface.
+            //
+            // Additionally for ingress all packets bound for the clat IPv6 address are dropped
+            // in ip6tables raw prerouting and thus even non-offloaded packets are only
+            // accounted for on the stacked interface.
+            //
+            // For egress, packets subject to eBPF offload never appear on the base interface
+            // and only appear on the stacked interface. Thus to ensure packets increment
+            // interface stats, we must collate data from stacked interfaces. For xt_qtaguid
+            // (or non eBPF offloaded) TX they would appear on both, however egress interface
+            // accounting is explicitly bypassed for traffic from the clat uid.
+            //
+            // TODO: This code might be combined to above code.
+            for (String iface : snapshot.getLinkProperties().getAllInterfaceNames()) {
+                // baseIface has been handled, so ignore it.
+                if (TextUtils.equals(baseIface, iface)) continue;
+                if (iface != null) {
+                    findOrCreateNetworkIdentitySet(mActiveIfaces, iface).add(ident);
+                    findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident);
+                    if (isMobile) {
+                        mobileIfaces.add(iface);
+                    }
+                    if (isWifi) {
+                        wifiIfaces.add(iface);
+                    }
+
+                    mStatsFactory.noteStackedIface(iface, baseIface);
+                }
+            }
+        }
+
+        mMobileIfaces = mobileIfaces.toArray(new String[0]);
+        mWifiIfaces = wifiIfaces.toArray(new String[0]);
+        // TODO (b/192758557): Remove debug log.
+        if (CollectionUtils.contains(mMobileIfaces, null)) {
+            throw new NullPointerException(
+                    "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
+        }
+        if (CollectionUtils.contains(mWifiIfaces, null)) {
+            throw new NullPointerException(
+                    "null element in mWifiIfaces: " + Arrays.toString(mWifiIfaces));
+        }
+    }
+
+    private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
+        if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR");
+        }
+
+        final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier();
+        if (spec instanceof TelephonyNetworkSpecifier) {
+             return ((TelephonyNetworkSpecifier) spec).getSubscriptionId();
+        } else {
+            Log.wtf(TAG, "getSubIdForState invalid NetworkSpecifier");
+            return INVALID_SUBSCRIPTION_ID;
+        }
+    }
+
+    /**
+     * For networks with {@code TRANSPORT_CELLULAR}, get ratType that was obtained through
+     * {@link PhoneStateListener}. Otherwise, return 0 given that other networks with different
+     * transport types do not actually fill this value.
+     */
+    private int getRatTypeForStateSnapshot(@NonNull NetworkStateSnapshot state) {
+        if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            return 0;
+        }
+
+        return mNetworkStatsSubscriptionsMonitor.getRatTypeForSubscriberId(state.getSubscriberId());
+    }
+
+    private static <K> NetworkIdentitySet findOrCreateNetworkIdentitySet(
+            ArrayMap<K, NetworkIdentitySet> map, K key) {
+        NetworkIdentitySet ident = map.get(key);
+        if (ident == null) {
+            ident = new NetworkIdentitySet();
+            map.put(key, ident);
+        }
+        return ident;
+    }
+
+    @GuardedBy("mStatsLock")
+    private void recordSnapshotLocked(long currentTime) throws RemoteException {
+        // snapshot and record current counters; read UID stats first to
+        // avoid over counting dev stats.
+        Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotUid");
+        final NetworkStats uidSnapshot = getNetworkStatsUidDetail(INTERFACES_ALL);
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+        Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotXt");
+        final NetworkStats xtSnapshot = readNetworkStatsSummaryXt();
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+        Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotDev");
+        final NetworkStats devSnapshot = readNetworkStatsSummaryDev();
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+
+        // Snapshot for dev/xt stats from all custom stats providers. Counts per-interface data
+        // from stats providers that isn't already counted by dev and XT stats.
+        Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotStatsProvider");
+        final NetworkStats providersnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+        xtSnapshot.combineAllValues(providersnapshot);
+        devSnapshot.combineAllValues(providersnapshot);
+
+        // For xt/dev, we pass a null VPN array because usage is aggregated by UID, so VPN traffic
+        // can't be reattributed to responsible apps.
+        Trace.traceBegin(TRACE_TAG_NETWORK, "recordDev");
+        mDevRecorder.recordSnapshotLocked(devSnapshot, mActiveIfaces, currentTime);
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+        Trace.traceBegin(TRACE_TAG_NETWORK, "recordXt");
+        mXtRecorder.recordSnapshotLocked(xtSnapshot, mActiveIfaces, currentTime);
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+
+        // For per-UID stats, pass the VPN info so VPN traffic is reattributed to responsible apps.
+        Trace.traceBegin(TRACE_TAG_NETWORK, "recordUid");
+        mUidRecorder.recordSnapshotLocked(uidSnapshot, mActiveUidIfaces, currentTime);
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+        Trace.traceBegin(TRACE_TAG_NETWORK, "recordUidTag");
+        mUidTagRecorder.recordSnapshotLocked(uidSnapshot, mActiveUidIfaces, currentTime);
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+
+        // We need to make copies of member fields that are sent to the observer to avoid
+        // a race condition between the service handler thread and the observer's
+        mStatsObservers.updateStats(xtSnapshot, uidSnapshot, new ArrayMap<>(mActiveIfaces),
+                new ArrayMap<>(mActiveUidIfaces), currentTime);
+    }
+
+    /**
+     * Bootstrap initial stats snapshot, usually during {@link #systemReady()}
+     * so we have baseline values without double-counting.
+     */
+    @GuardedBy("mStatsLock")
+    private void bootstrapStatsLocked() {
+        final long currentTime = mClock.millis();
+
+        try {
+            recordSnapshotLocked(currentTime);
+        } catch (IllegalStateException e) {
+            Log.w(TAG, "problem reading network stats: " + e);
+        } catch (RemoteException e) {
+            // ignored; service lives in system_server
+        }
+    }
+
+    private void performPoll(int flags) {
+        synchronized (mStatsLock) {
+            mWakeLock.acquire();
+
+            try {
+                performPollLocked(flags);
+            } finally {
+                mWakeLock.release();
+            }
+        }
+    }
+
+    /**
+     * Periodic poll operation, reading current statistics and recording into
+     * {@link NetworkStatsHistory}.
+     */
+    @GuardedBy("mStatsLock")
+    private void performPollLocked(int flags) {
+        if (!mSystemReady) return;
+        if (LOGV) Log.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")");
+        Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked");
+
+        final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0;
+        final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0;
+        final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0;
+
+        performPollFromProvidersLocked();
+
+        // TODO: consider marking "untrusted" times in historical stats
+        final long currentTime = mClock.millis();
+
+        try {
+            recordSnapshotLocked(currentTime);
+        } catch (IllegalStateException e) {
+            Log.wtf(TAG, "problem reading network stats", e);
+            return;
+        } catch (RemoteException e) {
+            // ignored; service lives in system_server
+            return;
+        }
+
+        // persist any pending data depending on requested flags
+        Trace.traceBegin(TRACE_TAG_NETWORK, "[persisting]");
+        if (persistForce) {
+            mDevRecorder.forcePersistLocked(currentTime);
+            mXtRecorder.forcePersistLocked(currentTime);
+            mUidRecorder.forcePersistLocked(currentTime);
+            mUidTagRecorder.forcePersistLocked(currentTime);
+        } else {
+            if (persistNetwork) {
+                mDevRecorder.maybePersistLocked(currentTime);
+                mXtRecorder.maybePersistLocked(currentTime);
+            }
+            if (persistUid) {
+                mUidRecorder.maybePersistLocked(currentTime);
+                mUidTagRecorder.maybePersistLocked(currentTime);
+            }
+        }
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+
+        if (mSettings.getSampleEnabled()) {
+            // sample stats after each full poll
+            performSampleLocked();
+        }
+
+        // finally, dispatch updated event to any listeners
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+    }
+
+    @GuardedBy("mStatsLock")
+    private void performPollFromProvidersLocked() {
+        // Request asynchronous stats update from all providers for next poll. And wait a bit of
+        // time to allow providers report-in given that normally binder call should be fast. Note
+        // that size of list might be changed because addition/removing at the same time. For
+        // addition, the stats of the missed provider can only be collected in next poll;
+        // for removal, wait might take up to MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS
+        // once that happened.
+        // TODO: request with a valid token.
+        Trace.traceBegin(TRACE_TAG_NETWORK, "provider.requestStatsUpdate");
+        final int registeredCallbackCount = mStatsProviderCbList.size();
+        mStatsProviderSem.drainPermits();
+        invokeForAllStatsProviderCallbacks(
+                (cb) -> cb.mProvider.onRequestStatsUpdate(0 /* unused */));
+        try {
+            mStatsProviderSem.tryAcquire(registeredCallbackCount,
+                    MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            // Strictly speaking it's possible a provider happened to deliver between the timeout
+            // and the log, and that doesn't matter too much as this is just a debug log.
+            Log.d(TAG, "requestStatsUpdate - providers responded "
+                    + mStatsProviderSem.availablePermits()
+                    + "/" + registeredCallbackCount + " : " + e);
+        }
+        Trace.traceEnd(TRACE_TAG_NETWORK);
+    }
+
+    /**
+     * Sample recent statistics summary into {@link EventLog}.
+     */
+    @GuardedBy("mStatsLock")
+    private void performSampleLocked() {
+        // TODO: migrate trustedtime fixes to separate binary log events
+        final long currentTime = mClock.millis();
+
+        NetworkTemplate template;
+        NetworkStats.Entry devTotal;
+        NetworkStats.Entry xtTotal;
+        NetworkStats.Entry uidTotal;
+
+        // collect mobile sample
+        template = buildTemplateMobileWildcard();
+        devTotal = mDevRecorder.getTotalSinceBootLocked(template);
+        xtTotal = mXtRecorder.getTotalSinceBootLocked(template);
+        uidTotal = mUidRecorder.getTotalSinceBootLocked(template);
+
+        EventLog.writeEvent(LOG_TAG_NETSTATS_MOBILE_SAMPLE,
+                devTotal.rxBytes, devTotal.rxPackets, devTotal.txBytes, devTotal.txPackets,
+                xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+                uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
+                currentTime);
+
+        // collect wifi sample
+        template = buildTemplateWifiWildcard();
+        devTotal = mDevRecorder.getTotalSinceBootLocked(template);
+        xtTotal = mXtRecorder.getTotalSinceBootLocked(template);
+        uidTotal = mUidRecorder.getTotalSinceBootLocked(template);
+
+        EventLog.writeEvent(LOG_TAG_NETSTATS_WIFI_SAMPLE,
+                devTotal.rxBytes, devTotal.rxPackets, devTotal.txBytes, devTotal.txPackets,
+                xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+                uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
+                currentTime);
+    }
+
+    // deleteKernelTagData can ignore ENOENT; otherwise we should log an error
+    private void logErrorIfNotErrNoent(final ErrnoException e, final String msg) {
+        if (e.errno != ENOENT) Log.e(TAG, msg, e);
+    }
+
+    private <K extends StatsMapKey, V extends StatsMapValue> void deleteStatsMapTagData(
+            IBpfMap<K, V> statsMap, int uid) {
+        try {
+            statsMap.forEach((key, value) -> {
+                if (key.uid == uid) {
+                    try {
+                        statsMap.deleteEntry(key);
+                    } catch (ErrnoException e) {
+                        logErrorIfNotErrNoent(e, "Failed to delete data(uid = " + key.uid + ")");
+                    }
+                }
+            });
+        } catch (ErrnoException e) {
+            Log.e(TAG, "FAILED to delete tag data from stats map", e);
+        }
+    }
+
+    /**
+     * Deletes uid tag data from CookieTagMap, StatsMapA, StatsMapB, and UidStatsMap
+     * @param uid
+     */
+    private void deleteKernelTagData(int uid) {
+        try {
+            mCookieTagMap.forEach((key, value) -> {
+                // If SkDestroyListener deletes the socket tag while this code is running,
+                // forEach will either restart iteration from the beginning or return null,
+                // depending on when the deletion happens.
+                // If it returns null, continue iteration to delete the data and in fact it would
+                // just iterate from first key because BpfMap#getNextKey would return first key
+                // if the current key is not exist.
+                if (value != null && value.uid == uid) {
+                    try {
+                        mCookieTagMap.deleteEntry(key);
+                    } catch (ErrnoException e) {
+                        logErrorIfNotErrNoent(e, "Failed to delete data(cookie = " + key + ")");
+                    }
+                }
+            });
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to delete tag data from cookie tag map", e);
+        }
+
+        deleteStatsMapTagData(mStatsMapA, uid);
+        deleteStatsMapTagData(mStatsMapB, uid);
+
+        try {
+            mUidCounterSetMap.deleteEntry(new U32(uid));
+        } catch (ErrnoException e) {
+            logErrorIfNotErrNoent(e, "Failed to delete tag data from uid counter set map");
+        }
+
+        try {
+            mAppUidStatsMap.deleteEntry(new UidStatsMapKey(uid));
+        } catch (ErrnoException e) {
+            logErrorIfNotErrNoent(e, "Failed to delete tag data from app uid stats map");
+        }
+    }
+
+    /**
+     * Clean up {@link #mUidRecorder} after UID is removed.
+     */
+    @GuardedBy("mStatsLock")
+    private void removeUidsLocked(int... uids) {
+        if (LOGV) Log.v(TAG, "removeUidsLocked() for UIDs " + Arrays.toString(uids));
+
+        // Perform one last poll before removing
+        performPollLocked(FLAG_PERSIST_ALL);
+
+        mUidRecorder.removeUidsLocked(uids);
+        mUidTagRecorder.removeUidsLocked(uids);
+
+        // Clear kernel stats associated with UID
+        for (int uid : uids) {
+            deleteKernelTagData(uid);
+        }
+    }
+
+    /**
+     * Clean up {@link #mUidRecorder} after user is removed.
+     */
+    @GuardedBy("mStatsLock")
+    private void removeUserLocked(@NonNull UserHandle userHandle) {
+        if (LOGV) Log.v(TAG, "removeUserLocked() for UserHandle=" + userHandle);
+
+        // Build list of UIDs that we should clean up
+        final ArrayList<Integer> uids = new ArrayList<>();
+        final List<ApplicationInfo> apps = mContext.getPackageManager().getInstalledApplications(
+                PackageManager.MATCH_ANY_USER
+                | PackageManager.MATCH_DISABLED_COMPONENTS);
+        for (ApplicationInfo app : apps) {
+            final int uid = userHandle.getUid(app.uid);
+            uids.add(uid);
+        }
+
+        removeUidsLocked(CollectionUtils.toIntArray(uids));
+    }
+
+    /**
+     * Set the warning and limit to all registered custom network stats providers.
+     * Note that invocation of any interface will be sent to all providers.
+     */
+    public void setStatsProviderWarningAndLimitAsync(
+            @NonNull String iface, long warning, long limit) {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        if (LOGV) {
+            Log.v(TAG, "setStatsProviderWarningAndLimitAsync("
+                    + iface + "," + warning + "," + limit + ")");
+        }
+        invokeForAllStatsProviderCallbacks((cb) -> cb.mProvider.onSetWarningAndLimit(iface,
+                warning, limit));
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter rawWriter, String[] args) {
+        if (!PermissionUtils.checkDumpPermission(mContext, TAG, rawWriter)) return;
+
+        long duration = DateUtils.DAY_IN_MILLIS;
+        final HashSet<String> argSet = new HashSet<String>();
+        for (String arg : args) {
+            argSet.add(arg);
+
+            if (arg.startsWith("--duration=")) {
+                try {
+                    duration = Long.parseLong(arg.substring(11));
+                } catch (NumberFormatException ignored) {
+                }
+            }
+        }
+
+        // usage: dumpsys netstats --full --uid --tag --poll --checkin
+        final boolean poll = argSet.contains("--poll") || argSet.contains("poll");
+        final boolean checkin = argSet.contains("--checkin");
+        final boolean fullHistory = argSet.contains("--full") || argSet.contains("full");
+        final boolean includeUid = argSet.contains("--uid") || argSet.contains("detail");
+        final boolean includeTag = argSet.contains("--tag") || argSet.contains("detail");
+
+        final IndentingPrintWriter pw = new IndentingPrintWriter(rawWriter, "  ");
+
+        synchronized (mStatsLock) {
+            if (args.length > 0 && "--proto".equals(args[0])) {
+                // In this case ignore all other arguments.
+                dumpProtoLocked(fd);
+                return;
+            }
+
+            if (poll) {
+                performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE);
+                pw.println("Forced poll");
+                return;
+            }
+
+            if (checkin) {
+                final long end = System.currentTimeMillis();
+                final long start = end - duration;
+
+                pw.print("v1,");
+                pw.print(start / SECOND_IN_MILLIS); pw.print(',');
+                pw.print(end / SECOND_IN_MILLIS); pw.println();
+
+                pw.println("xt");
+                mXtRecorder.dumpCheckin(rawWriter, start, end);
+
+                if (includeUid) {
+                    pw.println("uid");
+                    mUidRecorder.dumpCheckin(rawWriter, start, end);
+                }
+                if (includeTag) {
+                    pw.println("tag");
+                    mUidTagRecorder.dumpCheckin(rawWriter, start, end);
+                }
+                return;
+            }
+
+            pw.println("Configs:");
+            pw.increaseIndent();
+            pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
+            pw.println();
+            pw.decreaseIndent();
+
+            pw.println("Active interfaces:");
+            pw.increaseIndent();
+            for (int i = 0; i < mActiveIfaces.size(); i++) {
+                pw.print("iface", mActiveIfaces.keyAt(i));
+                pw.print("ident", mActiveIfaces.valueAt(i));
+                pw.println();
+            }
+            pw.decreaseIndent();
+
+            pw.println("Active UID interfaces:");
+            pw.increaseIndent();
+            for (int i = 0; i < mActiveUidIfaces.size(); i++) {
+                pw.print("iface", mActiveUidIfaces.keyAt(i));
+                pw.print("ident", mActiveUidIfaces.valueAt(i));
+                pw.println();
+            }
+            pw.decreaseIndent();
+
+            // Get the top openSession callers
+            final SparseIntArray calls;
+            synchronized (mOpenSessionCallsPerUid) {
+                calls = mOpenSessionCallsPerUid.clone();
+            }
+
+            final int N = calls.size();
+            final long[] values = new long[N];
+            for (int j = 0; j < N; j++) {
+                values[j] = ((long) calls.valueAt(j) << 32) | calls.keyAt(j);
+            }
+            Arrays.sort(values);
+
+            pw.println("Top openSession callers (uid=count):");
+            pw.increaseIndent();
+            final int end = Math.max(0, N - DUMP_STATS_SESSION_COUNT);
+            for (int j = N - 1; j >= end; j--) {
+                final int uid = (int) (values[j] & 0xffffffff);
+                final int count = (int) (values[j] >> 32);
+                pw.print(uid); pw.print("="); pw.println(count);
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            pw.println("Stats Providers:");
+            pw.increaseIndent();
+            invokeForAllStatsProviderCallbacks((cb) -> {
+                pw.println(cb.mTag + " Xt:");
+                pw.increaseIndent();
+                pw.print(cb.getCachedStats(STATS_PER_IFACE).toString());
+                pw.decreaseIndent();
+                if (includeUid) {
+                    pw.println(cb.mTag + " Uid:");
+                    pw.increaseIndent();
+                    pw.print(cb.getCachedStats(STATS_PER_UID).toString());
+                    pw.decreaseIndent();
+                }
+            });
+            pw.decreaseIndent();
+
+            pw.println("Dev stats:");
+            pw.increaseIndent();
+            mDevRecorder.dumpLocked(pw, fullHistory);
+            pw.decreaseIndent();
+
+            pw.println("Xt stats:");
+            pw.increaseIndent();
+            mXtRecorder.dumpLocked(pw, fullHistory);
+            pw.decreaseIndent();
+
+            if (includeUid) {
+                pw.println("UID stats:");
+                pw.increaseIndent();
+                mUidRecorder.dumpLocked(pw, fullHistory);
+                pw.decreaseIndent();
+            }
+
+            if (includeTag) {
+                pw.println("UID tag stats:");
+                pw.increaseIndent();
+                mUidTagRecorder.dumpLocked(pw, fullHistory);
+                pw.decreaseIndent();
+            }
+        }
+    }
+
+    @GuardedBy("mStatsLock")
+    private void dumpProtoLocked(FileDescriptor fd) {
+        final ProtoOutputStream proto = new ProtoOutputStream(new FileOutputStream(fd));
+
+        // TODO Right now it writes all history.  Should it limit to the "since-boot" log?
+
+        dumpInterfaces(proto, NetworkStatsServiceDumpProto.ACTIVE_INTERFACES,
+                mActiveIfaces);
+        dumpInterfaces(proto, NetworkStatsServiceDumpProto.ACTIVE_UID_INTERFACES,
+                mActiveUidIfaces);
+        mDevRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.DEV_STATS);
+        mXtRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.XT_STATS);
+        mUidRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.UID_STATS);
+        mUidTagRecorder.dumpDebugLocked(proto,
+                NetworkStatsServiceDumpProto.UID_TAG_STATS);
+
+        proto.flush();
+    }
+
+    private static void dumpInterfaces(ProtoOutputStream proto, long tag,
+            ArrayMap<String, NetworkIdentitySet> ifaces) {
+        for (int i = 0; i < ifaces.size(); i++) {
+            final long start = proto.start(tag);
+
+            proto.write(NetworkInterfaceProto.INTERFACE, ifaces.keyAt(i));
+            ifaces.valueAt(i).dumpDebug(proto, NetworkInterfaceProto.IDENTITIES);
+
+            proto.end(start);
+        }
+    }
+
+    private NetworkStats readNetworkStatsSummaryDev() {
+        try {
+            return mStatsFactory.readNetworkStatsSummaryDev();
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private NetworkStats readNetworkStatsSummaryXt() {
+        try {
+            return mStatsFactory.readNetworkStatsSummaryXt();
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private NetworkStats readNetworkStatsUidDetail(int uid, String[] ifaces, int tag) {
+        try {
+            return mStatsFactory.readNetworkStatsDetail(uid, ifaces, tag);
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Return snapshot of current UID statistics, including any
+     * {@link TrafficStats#UID_TETHERING}, video calling data usage, and {@link #mUidOperations}
+     * values.
+     *
+     * @param ifaces A list of interfaces the stats should be restricted to, or
+     *               {@link NetworkStats#INTERFACES_ALL}.
+     */
+    private NetworkStats getNetworkStatsUidDetail(String[] ifaces)
+            throws RemoteException {
+        final NetworkStats uidSnapshot = readNetworkStatsUidDetail(UID_ALL,  ifaces, TAG_ALL);
+
+        // fold tethering stats and operations into uid snapshot
+        final NetworkStats tetherSnapshot = getNetworkStatsTethering(STATS_PER_UID);
+        tetherSnapshot.filter(UID_ALL, ifaces, TAG_ALL);
+        mStatsFactory.apply464xlatAdjustments(uidSnapshot, tetherSnapshot);
+        uidSnapshot.combineAllValues(tetherSnapshot);
+
+        // get a stale copy of uid stats snapshot provided by providers.
+        final NetworkStats providerStats = getNetworkStatsFromProviders(STATS_PER_UID);
+        providerStats.filter(UID_ALL, ifaces, TAG_ALL);
+        mStatsFactory.apply464xlatAdjustments(uidSnapshot, providerStats);
+        uidSnapshot.combineAllValues(providerStats);
+
+        uidSnapshot.combineAllValues(mUidOperations);
+
+        return uidSnapshot;
+    }
+
+    /**
+     * Return snapshot of current non-offloaded tethering statistics. Will return empty
+     * {@link NetworkStats} if any problems are encountered, or queried by {@code STATS_PER_IFACE}
+     * since it is already included by {@link #nativeGetIfaceStat}.
+     * See {@code OffloadTetheringStatsProvider} for offloaded tethering stats.
+     */
+    // TODO: Remove this by implementing {@link NetworkStatsProvider} for non-offloaded
+    //  tethering stats.
+    private @NonNull NetworkStats getNetworkStatsTethering(int how) throws RemoteException {
+         // We only need to return per-UID stats. Per-device stats are already counted by
+        // interface counters.
+        if (how != STATS_PER_UID) {
+            return new NetworkStats(SystemClock.elapsedRealtime(), 0);
+        }
+
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 1);
+        try {
+            final TetherStatsParcel[] tetherStatsParcels = mNetd.tetherGetStats();
+            for (TetherStatsParcel tetherStats : tetherStatsParcels) {
+                try {
+                    stats.combineValues(new NetworkStats.Entry(tetherStats.iface, UID_TETHERING,
+                            SET_DEFAULT, TAG_NONE, tetherStats.rxBytes, tetherStats.rxPackets,
+                            tetherStats.txBytes, tetherStats.txPackets, 0L));
+                } catch (ArrayIndexOutOfBoundsException e) {
+                    throw new IllegalStateException("invalid tethering stats " + e);
+                }
+            }
+        } catch (IllegalStateException e) {
+            Log.wtf(TAG, "problem reading network stats", e);
+        }
+        return stats;
+    }
+
+    // TODO: It is copied from ConnectivityService, consider refactor these check permission
+    //  functions to a proper util.
+    private boolean checkAnyPermissionOf(String... permissions) {
+        for (String permission : permissions) {
+            if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void enforceAnyPermissionOf(String... permissions) {
+        if (!checkAnyPermissionOf(permissions)) {
+            throw new SecurityException("Requires one of the following permissions: "
+                    + String.join(", ", permissions) + ".");
+        }
+    }
+
+    /**
+     * Registers a custom provider of {@link android.net.NetworkStats} to combine the network
+     * statistics that cannot be seen by the kernel to system. To unregister, invoke the
+     * {@code unregister()} of the returned callback.
+     *
+     * @param tag a human readable identifier of the custom network stats provider.
+     * @param provider the {@link INetworkStatsProvider} binder corresponding to the
+     *                 {@link NetworkStatsProvider} to be registered.
+     *
+     * @return a {@link INetworkStatsProviderCallback} binder
+     *         interface, which can be used to report events to the system.
+     */
+    public @NonNull INetworkStatsProviderCallback registerNetworkStatsProvider(
+            @NonNull String tag, @NonNull INetworkStatsProvider provider) {
+        enforceAnyPermissionOf(NETWORK_STATS_PROVIDER,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+        Objects.requireNonNull(provider, "provider is null");
+        Objects.requireNonNull(tag, "tag is null");
+        final NetworkPolicyManager netPolicyManager = mContext
+                .getSystemService(NetworkPolicyManager.class);
+        try {
+            NetworkStatsProviderCallbackImpl callback = new NetworkStatsProviderCallbackImpl(
+                    tag, provider, mStatsProviderSem, mAlertObserver,
+                    mStatsProviderCbList, netPolicyManager);
+            mStatsProviderCbList.add(callback);
+            Log.d(TAG, "registerNetworkStatsProvider from " + callback.mTag + " uid/pid="
+                    + getCallingUid() + "/" + getCallingPid());
+            return callback;
+        } catch (RemoteException e) {
+            Log.e(TAG, "registerNetworkStatsProvider failed", e);
+        }
+        return null;
+    }
+
+    // Collect stats from local cache of providers.
+    private @NonNull NetworkStats getNetworkStatsFromProviders(int how) {
+        final NetworkStats ret = new NetworkStats(0L, 0);
+        invokeForAllStatsProviderCallbacks((cb) -> ret.combineAllValues(cb.getCachedStats(how)));
+        return ret;
+    }
+
+    @FunctionalInterface
+    private interface ThrowingConsumer<S, T extends Throwable> {
+        void accept(S s) throws T;
+    }
+
+    private void invokeForAllStatsProviderCallbacks(
+            @NonNull ThrowingConsumer<NetworkStatsProviderCallbackImpl, RemoteException> task) {
+        for (final NetworkStatsProviderCallbackImpl cb : mStatsProviderCbList) {
+            try {
+                task.accept(cb);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Fail to broadcast to provider: " + cb.mTag, e);
+            }
+        }
+    }
+
+    private static class NetworkStatsProviderCallbackImpl extends INetworkStatsProviderCallback.Stub
+            implements IBinder.DeathRecipient {
+        @NonNull final String mTag;
+
+        @NonNull final INetworkStatsProvider mProvider;
+        @NonNull private final Semaphore mSemaphore;
+        @NonNull final AlertObserver mAlertObserver;
+        @NonNull final CopyOnWriteArrayList<NetworkStatsProviderCallbackImpl> mStatsProviderCbList;
+        @NonNull final NetworkPolicyManager mNetworkPolicyManager;
+
+        @NonNull private final Object mProviderStatsLock = new Object();
+
+        @GuardedBy("mProviderStatsLock")
+        // Track STATS_PER_IFACE and STATS_PER_UID separately.
+        private final NetworkStats mIfaceStats = new NetworkStats(0L, 0);
+        @GuardedBy("mProviderStatsLock")
+        private final NetworkStats mUidStats = new NetworkStats(0L, 0);
+
+        NetworkStatsProviderCallbackImpl(
+                @NonNull String tag, @NonNull INetworkStatsProvider provider,
+                @NonNull Semaphore semaphore,
+                @NonNull AlertObserver alertObserver,
+                @NonNull CopyOnWriteArrayList<NetworkStatsProviderCallbackImpl> cbList,
+                @NonNull NetworkPolicyManager networkPolicyManager)
+                throws RemoteException {
+            mTag = tag;
+            mProvider = provider;
+            mProvider.asBinder().linkToDeath(this, 0);
+            mSemaphore = semaphore;
+            mAlertObserver = alertObserver;
+            mStatsProviderCbList = cbList;
+            mNetworkPolicyManager = networkPolicyManager;
+        }
+
+        @NonNull
+        public NetworkStats getCachedStats(int how) {
+            synchronized (mProviderStatsLock) {
+                NetworkStats stats;
+                switch (how) {
+                    case STATS_PER_IFACE:
+                        stats = mIfaceStats;
+                        break;
+                    case STATS_PER_UID:
+                        stats = mUidStats;
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Invalid type: " + how);
+                }
+                // Callers might be able to mutate the returned object. Return a defensive copy
+                // instead of local reference.
+                return stats.clone();
+            }
+        }
+
+        @Override
+        public void notifyStatsUpdated(int token, @Nullable NetworkStats ifaceStats,
+                @Nullable NetworkStats uidStats) {
+            // TODO: 1. Use token to map ifaces to correct NetworkIdentity.
+            //       2. Store the difference and store it directly to the recorder.
+            synchronized (mProviderStatsLock) {
+                if (ifaceStats != null) mIfaceStats.combineAllValues(ifaceStats);
+                if (uidStats != null) mUidStats.combineAllValues(uidStats);
+            }
+            mSemaphore.release();
+        }
+
+        @Override
+        public void notifyAlertReached() throws RemoteException {
+            // This binder object can only have been obtained by a process that holds
+            // NETWORK_STATS_PROVIDER. Thus, no additional permission check is required.
+            BinderUtils.withCleanCallingIdentity(() ->
+                    mAlertObserver.onQuotaLimitReached(LIMIT_GLOBAL_ALERT, null /* unused */));
+        }
+
+        @Override
+        public void notifyWarningReached() {
+            Log.d(TAG, mTag + ": notifyWarningReached");
+            BinderUtils.withCleanCallingIdentity(() ->
+                    mNetworkPolicyManager.notifyStatsProviderWarningReached());
+        }
+
+        @Override
+        public void notifyLimitReached() {
+            Log.d(TAG, mTag + ": notifyLimitReached");
+            BinderUtils.withCleanCallingIdentity(() ->
+                    mNetworkPolicyManager.notifyStatsProviderLimitReached());
+        }
+
+        @Override
+        public void binderDied() {
+            Log.d(TAG, mTag + ": binderDied");
+            mStatsProviderCbList.remove(this);
+        }
+
+        @Override
+        public void unregister() {
+            Log.d(TAG, mTag + ": unregister");
+            mStatsProviderCbList.remove(this);
+        }
+
+    }
+
+    private void assertSystemReady() {
+        if (!mSystemReady) {
+            throw new IllegalStateException("System not ready");
+        }
+    }
+
+    private class DropBoxNonMonotonicObserver implements NonMonotonicObserver<String> {
+        @Override
+        public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right,
+                int rightIndex, String cookie) {
+            Log.w(TAG, "Found non-monotonic values; saving to dropbox");
+
+            // record error for debugging
+            final StringBuilder builder = new StringBuilder();
+            builder.append("found non-monotonic " + cookie + " values at left[" + leftIndex
+                    + "] - right[" + rightIndex + "]\n");
+            builder.append("left=").append(left).append('\n');
+            builder.append("right=").append(right).append('\n');
+
+            mContext.getSystemService(DropBoxManager.class).addText(TAG_NETSTATS_ERROR,
+                    builder.toString());
+        }
+
+        @Override
+        public void foundNonMonotonic(
+                NetworkStats stats, int statsIndex, String cookie) {
+            Log.w(TAG, "Found non-monotonic values; saving to dropbox");
+
+            final StringBuilder builder = new StringBuilder();
+            builder.append("Found non-monotonic " + cookie + " values at [" + statsIndex + "]\n");
+            builder.append("stats=").append(stats).append('\n');
+
+            mContext.getSystemService(DropBoxManager.class).addText(TAG_NETSTATS_ERROR,
+                    builder.toString());
+        }
+    }
+
+    /**
+     * Default external settings that read from
+     * {@link android.provider.Settings.Global}.
+     */
+    private static class DefaultNetworkStatsSettings implements NetworkStatsSettings {
+        DefaultNetworkStatsSettings() {}
+
+        @Override
+        public long getPollInterval() {
+            return 30 * MINUTE_IN_MILLIS;
+        }
+        @Override
+        public long getPollDelay() {
+            return DEFAULT_PERFORM_POLL_DELAY_MS;
+        }
+        @Override
+        public long getGlobalAlertBytes(long def) {
+            return def;
+        }
+        @Override
+        public boolean getSampleEnabled() {
+            return true;
+        }
+        @Override
+        public boolean getAugmentEnabled() {
+            return true;
+        }
+        @Override
+        public boolean getCombineSubtypeEnabled() {
+            return false;
+        }
+        @Override
+        public Config getDevConfig() {
+            return new Config(HOUR_IN_MILLIS, 15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS);
+        }
+        @Override
+        public Config getXtConfig() {
+            return getDevConfig();
+        }
+        @Override
+        public Config getUidConfig() {
+            return new Config(2 * HOUR_IN_MILLIS, 15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS);
+        }
+        @Override
+        public Config getUidTagConfig() {
+            return new Config(2 * HOUR_IN_MILLIS, 5 * DAY_IN_MILLIS, 15 * DAY_IN_MILLIS);
+        }
+        @Override
+        public long getDevPersistBytes(long def) {
+            return def;
+        }
+        @Override
+        public long getXtPersistBytes(long def) {
+            return def;
+        }
+        @Override
+        public long getUidPersistBytes(long def) {
+            return def;
+        }
+        @Override
+        public long getUidTagPersistBytes(long def) {
+            return def;
+        }
+    }
+
+    private static native long nativeGetTotalStat(int type);
+    private static native long nativeGetIfaceStat(String iface, int type);
+    private static native long nativeGetUidStat(int uid, int type);
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java b/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java
new file mode 100644
index 0000000..65ccd20
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.app.usage.NetworkStatsManager.NETWORK_TYPE_5G_NSA;
+import static android.app.usage.NetworkStatsManager.getCollapsedRatType;
+import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED;
+import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA;
+import static android.telephony.TelephonyManager.NETWORK_TYPE_LTE;
+
+import android.annotation.NonNull;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyDisplayInfo;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * Helper class that watches for events that are triggered per subscription.
+ */
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class NetworkStatsSubscriptionsMonitor extends
+        SubscriptionManager.OnSubscriptionsChangedListener {
+
+    /**
+     * Interface that this monitor uses to delegate event handling to NetworkStatsService.
+     */
+    public interface Delegate {
+        /**
+         * Notify that the collapsed RAT type has been changed for any subscription. The method
+         * will also be triggered for any existing sub when start and stop monitoring.
+         *
+         * @param subscriberId IMSI of the subscription.
+         * @param collapsedRatType collapsed RAT type.
+         *                     @see android.app.usage.NetworkStatsManager#getCollapsedRatType(int).
+         */
+        void onCollapsedRatTypeChanged(@NonNull String subscriberId, int collapsedRatType);
+    }
+    private final Delegate mDelegate;
+
+    /**
+     * Receivers that watches for {@link TelephonyDisplayInfo} changes for each subscription, to
+     * monitor the transitioning between Radio Access Technology(RAT) types for each sub.
+     */
+    @NonNull
+    private final CopyOnWriteArrayList<RatTypeListener> mRatListeners =
+            new CopyOnWriteArrayList<>();
+
+    @NonNull
+    private final SubscriptionManager mSubscriptionManager;
+    @NonNull
+    private final TelephonyManager mTeleManager;
+
+    @NonNull
+    private final Executor mExecutor;
+
+    NetworkStatsSubscriptionsMonitor(@NonNull Context context,
+            @NonNull Executor executor, @NonNull Delegate delegate) {
+        super();
+        mSubscriptionManager = (SubscriptionManager) context.getSystemService(
+                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        mTeleManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        mExecutor = executor;
+        mDelegate = delegate;
+    }
+
+    @Override
+    public void onSubscriptionsChanged() {
+        // Collect active subId list, hidden subId such as opportunistic subscriptions are
+        // also needed to track CBRS.
+        final List<Integer> newSubs = getActiveSubIdList(mSubscriptionManager);
+
+        // IMSI is needed for every newly added sub. Listener stores subscriberId into it to
+        // prevent binder call to telephony when querying RAT. Keep listener registration with empty
+        // IMSI is meaningless since the RAT type changed is ambiguous for multi-SIM if reported
+        // with empty IMSI. So filter the subs w/o a valid IMSI to prevent such registration.
+        final List<Pair<Integer, String>> filteredNewSubs = new ArrayList<>();
+        for (final int subId : newSubs) {
+            final String subscriberId =
+                    mTeleManager.createForSubscriptionId(subId).getSubscriberId();
+            if (!TextUtils.isEmpty(subscriberId)) {
+                filteredNewSubs.add(new Pair(subId, subscriberId));
+            }
+        }
+
+        for (final Pair<Integer, String> sub : filteredNewSubs) {
+            // Fully match listener with subId and IMSI, since in some rare cases, IMSI might be
+            // suddenly change regardless of subId, such as switch IMSI feature in modem side.
+            // If that happens, register new listener with new IMSI and remove old one later.
+            if (CollectionUtils.any(mRatListeners, it -> it.equalsKey(sub.first, sub.second))) {
+                continue;
+            }
+
+            final RatTypeListener listener = new RatTypeListener(this, sub.first, sub.second);
+            mRatListeners.add(listener);
+
+            // Register listener to the telephony manager that associated with specific sub.
+            mTeleManager.createForSubscriptionId(sub.first)
+                    .registerTelephonyCallback(mExecutor, listener);
+            Log.d(NetworkStatsService.TAG, "RAT type listener registered for sub " + sub.first);
+        }
+
+        for (final RatTypeListener listener : new ArrayList<>(mRatListeners)) {
+            // If there is no subId and IMSI matched the listener, removes it.
+            if (!CollectionUtils.any(filteredNewSubs,
+                    it -> listener.equalsKey(it.first, it.second))) {
+                handleRemoveRatTypeListener(listener);
+            }
+        }
+    }
+
+    @NonNull
+    private List<Integer> getActiveSubIdList(@NonNull SubscriptionManager subscriptionManager) {
+        final ArrayList<Integer> ret = new ArrayList<>();
+        final int[] ids = subscriptionManager.getCompleteActiveSubscriptionIdList();
+        for (int id : ids) ret.add(id);
+        return ret;
+    }
+
+    /**
+     * Get a collapsed RatType for the given subscriberId.
+     *
+     * @param subscriberId the target subscriberId
+     * @return collapsed RatType for the given subscriberId
+     */
+    public int getRatTypeForSubscriberId(@NonNull String subscriberId) {
+        final int index = CollectionUtils.indexOf(mRatListeners,
+                it -> TextUtils.equals(subscriberId, it.mSubscriberId));
+        return index != -1 ? mRatListeners.get(index).mLastCollapsedRatType
+                : TelephonyManager.NETWORK_TYPE_UNKNOWN;
+    }
+
+    /**
+     * Start monitoring events that triggered per subscription.
+     */
+    public void start() {
+        mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor, this);
+    }
+
+    /**
+     * Unregister subscription changes and all listeners for each subscription.
+     */
+    public void stop() {
+        mSubscriptionManager.removeOnSubscriptionsChangedListener(this);
+
+        for (final RatTypeListener listener : new ArrayList<>(mRatListeners)) {
+            handleRemoveRatTypeListener(listener);
+        }
+    }
+
+    private void handleRemoveRatTypeListener(@NonNull RatTypeListener listener) {
+        mTeleManager.createForSubscriptionId(listener.mSubId)
+                .unregisterTelephonyCallback(listener);
+        Log.d(NetworkStatsService.TAG, "RAT type listener unregistered for sub " + listener.mSubId);
+        mRatListeners.remove(listener);
+
+        // Removal of subscriptions doesn't generate RAT changed event, fire it for every
+        // RatTypeListener.
+        mDelegate.onCollapsedRatTypeChanged(
+                listener.mSubscriberId, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+    }
+
+    static class RatTypeListener extends TelephonyCallback
+            implements TelephonyCallback.DisplayInfoListener {
+        // Unique id for the subscription. See {@link SubscriptionInfo#getSubscriptionId}.
+        @NonNull
+        private final int mSubId;
+
+        // IMSI to identifying the corresponding network from {@link NetworkState}.
+        // See {@link TelephonyManager#getSubscriberId}.
+        @NonNull
+        private final String mSubscriberId;
+
+        private volatile int mLastCollapsedRatType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        @NonNull
+        private final NetworkStatsSubscriptionsMonitor mMonitor;
+
+        RatTypeListener(@NonNull NetworkStatsSubscriptionsMonitor monitor, int subId,
+                @NonNull String subscriberId) {
+            mSubId = subId;
+            mSubscriberId = subscriberId;
+            mMonitor = monitor;
+        }
+
+        @Override
+        public void onDisplayInfoChanged(TelephonyDisplayInfo displayInfo) {
+            // In 5G SA (Stand Alone) mode, the primary cell itself will be 5G hence telephony
+            // would report RAT = 5G_NR.
+            // However, in 5G NSA (Non Stand Alone) mode, the primary cell is still LTE and
+            // network allocates a secondary 5G cell so telephony reports RAT = LTE along with
+            // NR state as connected. In such case, attributes the data usage to NR.
+            // See b/160727498.
+            final boolean is5GNsa = displayInfo.getNetworkType() == NETWORK_TYPE_LTE
+                    && (displayInfo.getOverrideNetworkType() == OVERRIDE_NETWORK_TYPE_NR_NSA
+                    || displayInfo.getOverrideNetworkType() == OVERRIDE_NETWORK_TYPE_NR_ADVANCED);
+
+            final int networkType =
+                    (is5GNsa ? NETWORK_TYPE_5G_NSA : displayInfo.getNetworkType());
+            final int collapsedRatType = getCollapsedRatType(networkType);
+            if (collapsedRatType == mLastCollapsedRatType) return;
+
+            if (NetworkStatsService.LOGD) {
+                Log.d(NetworkStatsService.TAG, "subtype changed for sub(" + mSubId + "): "
+                        + mLastCollapsedRatType + " -> " + collapsedRatType);
+            }
+            mLastCollapsedRatType = collapsedRatType;
+            mMonitor.mDelegate.onCollapsedRatTypeChanged(mSubscriberId, mLastCollapsedRatType);
+        }
+
+        @VisibleForTesting
+        public int getSubId() {
+            return mSubId;
+        }
+
+        boolean equalsKey(int subId, @NonNull String subscriberId) {
+            return mSubId == subId && TextUtils.equals(mSubscriberId, subscriberId);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/net/StatsMapKey.java b/service-t/src/com/android/server/net/StatsMapKey.java
new file mode 100644
index 0000000..ea8d836
--- /dev/null
+++ b/service-t/src/com/android/server/net/StatsMapKey.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * Key for both stats maps.
+ */
+public class StatsMapKey extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long uid;
+
+    @Field(order = 1, type = Type.U32)
+    public final long tag;
+
+    @Field(order = 2, type = Type.U32)
+    public final long counterSet;
+
+    @Field(order = 3, type = Type.U32)
+    public final long ifaceIndex;
+
+    public StatsMapKey(final long uid, final long tag, final long counterSet,
+            final long ifaceIndex) {
+        this.uid = uid;
+        this.tag = tag;
+        this.counterSet = counterSet;
+        this.ifaceIndex = ifaceIndex;
+    }
+}
diff --git a/service-t/src/com/android/server/net/StatsMapValue.java b/service-t/src/com/android/server/net/StatsMapValue.java
new file mode 100644
index 0000000..48f26ce
--- /dev/null
+++ b/service-t/src/com/android/server/net/StatsMapValue.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * Value used for both stats maps and uid stats map.
+ */
+public class StatsMapValue extends Struct {
+    @Field(order = 0, type = Type.U63)
+    public final long rxPackets;
+
+    @Field(order = 1, type = Type.U63)
+    public final long rxBytes;
+
+    @Field(order = 2, type = Type.U63)
+    public final long txPackets;
+
+    @Field(order = 3, type = Type.U63)
+    public final long txBytes;
+
+    public StatsMapValue(final long rxPackets, final long rxBytes, final long txPackets,
+            final long txBytes) {
+        this.rxPackets = rxPackets;
+        this.rxBytes = rxBytes;
+        this.txPackets = txPackets;
+        this.txBytes = txBytes;
+    }
+}
diff --git a/service-t/src/com/android/server/net/UidStatsMapKey.java b/service-t/src/com/android/server/net/UidStatsMapKey.java
new file mode 100644
index 0000000..2849f94
--- /dev/null
+++ b/service-t/src/com/android/server/net/UidStatsMapKey.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * Key for uid stats map.
+ */
+public class UidStatsMapKey extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long uid;
+
+    public UidStatsMapKey(final long uid) {
+        this.uid = uid;
+    }
+}
diff --git a/service/Android.bp b/service/Android.bp
index 33fe3ea..25b970a 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -19,6 +19,54 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+aidl_interface {
+    name: "connectivity_native_aidl_interface",
+    local_include_dir: "binder",
+    vendor_available: true,
+    srcs: [
+        "binder/android/net/connectivity/aidl/*.aidl",
+    ],
+    backend: {
+        java: {
+            apex_available: [
+                "com.android.tethering",
+            ],
+            min_sdk_version: "30",
+        },
+        ndk: {
+            apex_available: [
+                "com.android.tethering",
+            ],
+            min_sdk_version: "30",
+        },
+    },
+    versions: ["1"],
+
+}
+
+cc_library_static {
+    name: "connectivity_native_aidl_interface-lateststable-ndk",
+    min_sdk_version: "30",
+    whole_static_libs: [
+        "connectivity_native_aidl_interface-V1-ndk",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
+java_library {
+    name: "connectivity_native_aidl_interface-lateststable-java",
+    sdk_version: "system_current",
+    min_sdk_version: "30",
+    static_libs: [
+        "connectivity_native_aidl_interface-V1-java",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
 // The library name match the service-connectivity jarjar rules that put the JNI utils in the
 // android.net.connectivity.com.android.net.module.util package.
 cc_library_shared {
@@ -35,6 +83,7 @@
     ],
     static_libs: [
         "libnet_utils_device_common_bpfjni",
+        "libnet_utils_device_common_bpfutils",
     ],
     shared_libs: [
         "liblog",
@@ -55,6 +104,7 @@
         "-Wthread-safety",
     ],
     srcs: [
+        ":services.connectivity-netstats-jni-sources",
         "jni/com_android_server_BpfNetMaps.cpp",
         "jni/com_android_server_connectivity_ClatCoordinator.cpp",
         "jni/com_android_server_TestNetworkService.cpp",
@@ -66,15 +116,19 @@
     static_libs: [
         "libclat",
         "libip_checksum",
+        "libmodules-utils-build",
         "libnetjniutils",
+        "libnet_utils_device_common_bpfjni",
         "libtraffic_controller",
         "netd_aidl_interface-lateststable-ndk",
     ],
     shared_libs: [
         "libbase",
+        "libcutils",
         "libnetdutils",
         "liblog",
         "libnativehelper",
+        "libnetworkstats",
     ],
     apex_available: [
         "com.android.tethering",
@@ -95,6 +149,7 @@
     libs: [
         "framework-annotations-lib",
         "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t.stubs.module_lib",
         "framework-tethering.stubs.module_lib",
         "framework-wifi.stubs.module_lib",
         "unsupportedappusage",
@@ -103,11 +158,13 @@
     static_libs: [
         // Do not add libs here if they are already included
         // in framework-connectivity
+        "connectivity_native_aidl_interface-lateststable-java",
         "dnsresolver_aidl_interface-V9-java",
         "modules-utils-shell-command-handler",
         "net-utils-device-common",
         "net-utils-device-common-bpf",
         "net-utils-device-common-netlink",
+        "net-utils-services-common",
         "netd-client",
         "networkstack-client",
         "PlatformProperties",
@@ -153,7 +210,7 @@
     static_libs: [
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
-        "service-nearby",
+        "service-nearby-pre-jarjar",
     ],
     jarjar_rules: ":connectivity-jarjar-rules",
     apex_available: [
diff --git a/service/ServiceConnectivityResources/res/values-lv/strings.xml b/service/ServiceConnectivityResources/res/values-lv/strings.xml
index 9d26c40..ce063a5 100644
--- a/service/ServiceConnectivityResources/res/values-lv/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-lv/strings.xml
@@ -23,7 +23,7 @@
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
     <string name="wifi_no_internet" msgid="1326348603404555475">"Tīklā <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nav piekļuves internetam"</string>
-    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Pieskarieties, lai skatītu iespējas."</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Pieskarieties, lai skatītu opcijas."</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilajā tīklā nav piekļuves internetam."</string>
     <string name="other_networks_no_internet" msgid="5693932964749676542">"Tīklā nav piekļuves internetam."</string>
     <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Nevar piekļūt privātam DNS serverim."</string>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f8f86a2..81782f9 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -136,4 +136,47 @@
          extreme adverse effects on performance of the new network.
     -->
     <bool translatable="false" name="config_cellular_radio_timesharing_capable">true</bool>
+
+    <!-- Configure ethernet tcp buffersizes in the form:
+         rmem_min,rmem_def,rmem_max,wmem_min,wmem_def,wmem_max -->
+    <string translatable="false" name="config_ethernet_tcp_buffers">524288,1048576,3145728,524288,1048576,2097152</string>
+
+    <!-- Configuration of Ethernet interfaces in the following format:
+         <interface name|mac address>;[Network Capabilities];[IP config];[Override Transport]
+         Where
+               [Network Capabilities] Optional. A comma separated list of network capabilities.
+                   Values must be from NetworkCapabilities#NET_CAPABILITY_* constants.
+                   The NOT_ROAMING, NOT_CONGESTED and NOT_SUSPENDED capabilities are always
+                   added automatically because this configuration provides no way to update
+                   them dynamically.
+               [IP config] Optional. If empty or not specified - DHCP will be used, otherwise
+                   use the following format to specify static IP configuration:
+                       ip=<ip-address/mask> gateway=<ip-address> dns=<comma-sep-ip-addresses>
+                       domains=<comma-sep-domains>
+               [Override Transport] Optional. An override network transport type to allow
+                    the propagation of an interface type on the other end of a local Ethernet
+                    interface. Value must be from NetworkCapabilities#TRANSPORT_* constants. If
+                    left out, this will default to TRANSPORT_ETHERNET.
+         -->
+    <string-array translatable="false" name="config_ethernet_interfaces">
+        <!--
+        <item>eth1;12,13,14,15;ip=192.168.0.10/24 gateway=192.168.0.1 dns=4.4.4.4,8.8.8.8</item>
+        <item>eth2;;ip=192.168.0.11/24</item>
+        <item>eth3;12,13,14,15;ip=192.168.0.12/24;1</item>
+        -->
+    </string-array>
+
+    <!-- Regex of wired ethernet ifaces -->
+    <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+
+    <!-- Ignores Wi-Fi validation failures after roam.
+    If validation fails on a Wi-Fi network after a roam to a new BSSID,
+    assume that the roam temporarily disrupted network connectivity, and
+    ignore all failures until this time has passed.
+    NetworkMonitor will continue to attempt validation, and if it fails after this time has passed,
+    the network will be marked unvalidated.
+
+    Only supported up to S. On T+, the Wi-Fi code should use unregisterAfterReplacement in order
+    to ensure that apps see the network disconnect and reconnect. -->
+    <integer translatable="false" name="config_validationFailureAfterRoamIgnoreTimeMillis">-1</integer>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index e5010d7..b92dd08 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -37,6 +37,10 @@
             <item type="drawable" name="stat_notify_wifi_in_range"/>
             <item type="drawable" name="stat_notify_rssi_in_range"/>
             <item type="bool" name="config_cellular_radio_timesharing_capable" />
+            <item type="string" name="config_ethernet_tcp_buffers"/>
+            <item type="array" name="config_ethernet_interfaces"/>
+            <item type="string" name="config_ethernet_iface_regex"/>
+            <item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/aidl_api/connectivity_native_aidl_interface/1/.hash b/service/aidl_api/connectivity_native_aidl_interface/1/.hash
new file mode 100644
index 0000000..4625b4b
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/1/.hash
@@ -0,0 +1 @@
+037b467eb02b172a3161e11bbc3dd691aebb5fce
diff --git a/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..b3985a4
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.connectivity.aidl;
+interface ConnectivityNative {
+  void blockPortForBind(in int port);
+  void unblockPortForBind(in int port);
+  void unblockAllPortsForBind();
+  int[] getPortsBlockedForBind();
+}
diff --git a/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..b3985a4
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.connectivity.aidl;
+interface ConnectivityNative {
+  void blockPortForBind(in int port);
+  void unblockPortForBind(in int port);
+  void unblockAllPortsForBind();
+  int[] getPortsBlockedForBind();
+}
diff --git a/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..31e24b4
--- /dev/null
+++ b/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.connectivity.aidl;
+
+interface ConnectivityNative {
+    /**
+     * Blocks a port from being assigned during bind(). The caller is responsible for updating
+     * /proc/sys/net/ipv4/ip_local_port_range with the port being blocked so that calls to connect()
+     * will not automatically assign one of the blocked ports.
+     * Will return success even if port was already blocked.
+     *
+     * @param port Int corresponding to port number.
+     *
+     * @throws IllegalArgumentException if the port is invalid.
+     * @throws SecurityException if the UID of the client doesn't have network stack permission.
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void blockPortForBind(in int port);
+
+    /**
+     * Unblocks a port that has previously been blocked.
+     * Will return success even if port was already unblocked.
+     *
+     * @param port Int corresponding to port number.
+     *
+     * @throws IllegalArgumentException if the port is invalid.
+     * @throws SecurityException if the UID of the client doesn't have network stack permission.
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void unblockPortForBind(in int port);
+
+    /**
+     * Unblocks all ports that have previously been blocked.
+     */
+    void unblockAllPortsForBind();
+
+    /**
+     * Gets the list of ports that have been blocked.
+     *
+     * @return List of blocked ports.
+     */
+    int[] getPortsBlockedForBind();
+}
\ No newline at end of file
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index e3b26fd..4b21569 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -9,19 +9,22 @@
 # From modules-utils-preconditions
 rule com.android.internal.util.Preconditions* android.net.connectivity.@0
 
+# From framework-connectivity-shared-srcs
+rule android.util.LocalLog* android.net.connectivity.@0
+rule android.util.IndentingPrintWriter* android.net.connectivity.@0
+rule com.android.internal.util.IndentingPrintWriter* android.net.connectivity.@0
+rule com.android.internal.util.MessageUtils* android.net.connectivity.@0
+rule com.android.internal.util.WakeupMessage* android.net.connectivity.@0
+rule com.android.internal.util.FileRotator* android.net.connectivity.@0
+rule com.android.internal.util.ProcFileReader* android.net.connectivity.@0
+
+# From framework-connectivity-protos
+rule com.google.protobuf.** android.net.connectivity.@0
+rule android.service.** android.net.connectivity.@0
+
 rule android.sysprop.** com.android.connectivity.@0
 
-# internal util classes from framework-connectivity-shared-srcs
-rule android.util.LocalLog* com.android.connectivity.@0
-# android.util.IndentingPrintWriter* should use a different package name from
-# the one in com.android.internal.util
-rule android.util.IndentingPrintWriter* com.android.connectivity.@0
-rule com.android.internal.util.IndentingPrintWriter* com.android.connectivity.@0
-rule com.android.internal.util.MessageUtils* com.android.connectivity.@0
-rule com.android.internal.util.WakeupMessage* com.android.connectivity.@0
-
 rule com.android.internal.messages.** com.android.connectivity.@0
-rule com.google.protobuf.** com.android.connectivity.@0
 
 # From dnsresolver_aidl_interface (newer AIDLs should go to android.net.resolv.aidl)
 rule android.net.resolv.aidl.** com.android.connectivity.@0
@@ -89,7 +92,6 @@
 rule android.net.util.KeepalivePacketDataUtil* com.android.connectivity.@0
 
 # From connectivity-module-utils
-rule android.net.util.InterfaceParams* com.android.connectivity.@0
 rule android.net.util.SharedLog* com.android.connectivity.@0
 rule android.net.shared.** com.android.connectivity.@0
 
@@ -103,5 +105,11 @@
 # From the API shims
 rule com.android.networkstack.apishim.** com.android.connectivity.@0
 
+# From filegroup framework-connectivity-protos
+rule android.service.*Proto com.android.connectivity.@0
+
+# From mdns-aidl-interface
+rule android.net.mdns.aidl.** android.net.connectivity.@0
+
 # Remaining are connectivity sources in com.android.server and com.android.server.connectivity:
 # TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_net_module_util/onload.cpp b/service/jni/com_android_net_module_util/onload.cpp
index 2f09e55..d91eb03 100644
--- a/service/jni/com_android_net_module_util/onload.cpp
+++ b/service/jni/com_android_net_module_util/onload.cpp
@@ -21,6 +21,7 @@
 
 int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
 int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_BpfUtils(JNIEnv* env, char const* class_name);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
@@ -35,6 +36,9 @@
     if (register_com_android_net_module_util_TcUtils(env,
             "android/net/connectivity/com/android/net/module/util/TcUtils") < 0) return JNI_ERR;
 
+    if (register_com_android_net_module_util_BpfUtils(env,
+            "android/net/connectivity/com/android/net/module/util/BpfUtils") < 0) return JNI_ERR;
+
     return JNI_VERSION_1_6;
 }
 
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index 85cfc09..f13c68d 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -24,6 +24,7 @@
 #include <nativehelper/JNIHelp.h>
 #include <nativehelper/ScopedUtfChars.h>
 #include <nativehelper/ScopedPrimitiveArray.h>
+#include <netjniutils/netjniutils.h>
 #include <net/if.h>
 #include <vector>
 
@@ -108,6 +109,7 @@
     }
 
     size_t size = uids.size();
+    static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data);
     if (res) {
@@ -144,6 +146,7 @@
     }
 
     size_t size = uids.size();
+    static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     Status status = mTc.addUidInterfaceRules(ifIndex, data);
     if (!isOk(status)) {
@@ -159,6 +162,7 @@
     }
 
     size_t size = uids.size();
+    static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     Status status = mTc.removeUidInterfaceRules(data);
     if (!isOk(status)) {
@@ -186,6 +190,15 @@
     mTc.setPermissionForUids(permission, data);
 }
 
+static void native_dump(JNIEnv* env, jobject clazz, jobject javaFd, jboolean verbose) {
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
+    if (fd < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
+        return;
+    }
+    mTc.dump(fd, verbose);
+}
+
 /*
  * JNI registration.
  */
@@ -216,6 +229,8 @@
     (void*)native_swapActiveStatsMap},
     {"native_setPermissionForUids", "(I[I)V",
     (void*)native_setPermissionForUids},
+    {"native_dump", "(Ljava/io/FileDescriptor;Z)V",
+    (void*)native_dump},
 };
 // clang-format on
 
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index ee512ec..500c696 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -18,6 +18,7 @@
 #include <arpa/inet.h>
 #include <errno.h>
 #include <fcntl.h>
+#include <inttypes.h>
 #include <linux/if_tun.h>
 #include <linux/ioctl.h>
 #include <log/log.h>
@@ -27,9 +28,12 @@
 #include <sys/wait.h>
 #include <string>
 
+#include <bpf/BpfMap.h>
+#include <bpf/BpfUtils.h>
+#include <bpf_shared.h>
 #include <netjniutils/netjniutils.h>
+#include <private/android_filesystem_config.h>
 
-#include "libclat/bpfhelper.h"
 #include "libclat/clatutils.h"
 #include "nativehelper/scoped_utf_chars.h"
 
@@ -252,46 +256,6 @@
     }
 }
 
-int initTracker(const std::string& iface, const std::string& pfx96, const std::string& v4,
-        const std::string& v6, net::clat::ClatdTracker* output) {
-    strlcpy(output->iface, iface.c_str(), sizeof(output->iface));
-    output->ifIndex = if_nametoindex(iface.c_str());
-    if (output->ifIndex == 0) {
-        ALOGE("interface %s not found", output->iface);
-        return -1;
-    }
-
-    unsigned len = snprintf(output->v4iface, sizeof(output->v4iface),
-            "%s%s", DEVICEPREFIX, iface.c_str());
-    if (len >= sizeof(output->v4iface)) {
-        ALOGE("interface name too long '%s'", output->v4iface);
-        return -1;
-    }
-
-    output->v4ifIndex = if_nametoindex(output->v4iface);
-    if (output->v4ifIndex == 0) {
-        ALOGE("v4-interface %s not found", output->v4iface);
-        return -1;
-    }
-
-    if (!inet_pton(AF_INET6, pfx96.c_str(), &output->pfx96)) {
-        ALOGE("invalid IPv6 address specified for plat prefix: %s", pfx96.c_str());
-        return -1;
-    }
-
-    if (!inet_pton(AF_INET, v4.c_str(), &output->v4)) {
-        ALOGE("Invalid IPv4 address %s", v4.c_str());
-        return -1;
-    }
-
-    if (!inet_pton(AF_INET6, v6.c_str(), &output->v6)) {
-        ALOGE("Invalid source address %s", v6.c_str());
-        return -1;
-    }
-
-    return 0;
-}
-
 static jint com_android_server_connectivity_ClatCoordinator_startClatd(
         JNIEnv* env, jobject clazz, jobject tunJavaFd, jobject readSockJavaFd,
         jobject writeSockJavaFd, jstring iface, jstring pfx96, jstring v4, jstring v6) {
@@ -318,30 +282,13 @@
         return -1;
     }
 
-    // 1. create a throwaway socket to reserve a file descriptor number
-    int passedTunFd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
-    if (passedTunFd == -1) {
-        throwIOException(env, "socket(ipv6/udp) for tun fd failed", errno);
-        return -1;
-    }
-    int passedSockRead = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
-    if (passedSockRead == -1) {
-        throwIOException(env, "socket(ipv6/udp) for read socket failed", errno);
-        return -1;
-    }
-    int passedSockWrite = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
-    if (passedSockWrite == -1) {
-        throwIOException(env, "socket(ipv6/udp) for write socket failed", errno);
-        return -1;
-    }
-
-    // these are the FD we'll pass to clatd on the cli, so need it as a string
-    char passedTunFdStr[INT32_STRLEN];
-    char passedSockReadStr[INT32_STRLEN];
-    char passedSockWriteStr[INT32_STRLEN];
-    snprintf(passedTunFdStr, sizeof(passedTunFdStr), "%d", passedTunFd);
-    snprintf(passedSockReadStr, sizeof(passedSockReadStr), "%d", passedSockRead);
-    snprintf(passedSockWriteStr, sizeof(passedSockWriteStr), "%d", passedSockWrite);
+    // 1. these are the FD we'll pass to clatd on the cli, so need it as a string
+    char tunFdStr[INT32_STRLEN];
+    char sockReadStr[INT32_STRLEN];
+    char sockWriteStr[INT32_STRLEN];
+    snprintf(tunFdStr, sizeof(tunFdStr), "%d", tunFd);
+    snprintf(sockReadStr, sizeof(sockReadStr), "%d", readSock);
+    snprintf(sockWriteStr, sizeof(sockWriteStr), "%d", writeSock);
 
     // 2. we're going to use this as argv[0] to clatd to make ps output more useful
     std::string progname("clatd-");
@@ -353,9 +300,9 @@
                           "-p", pfx96Str.c_str(),
                           "-4", v4Str.c_str(),
                           "-6", v6Str.c_str(),
-                          "-t", passedTunFdStr,
-                          "-r", passedSockReadStr,
-                          "-w", passedSockWriteStr,
+                          "-t", tunFdStr,
+                          "-r", sockReadStr,
+                          "-w", sockWriteStr,
                           nullptr};
     // clang-format on
 
@@ -375,7 +322,9 @@
 
     // 4. register dup2() action: this is what 'clears' the CLOEXEC flag
     // on the tun fd that we want the child clatd process to inherit
-    // (this will happen after the vfork, and before the execve)
+    // (this will happen after the vfork, and before the execve).
+    // Note that even though dup2(2) is a no-op if fd == new_fd but O_CLOEXEC flag will be removed.
+    // See implementation of bionic's posix_spawn_file_actions_adddup2().
     posix_spawn_file_actions_t fa;
     if (int ret = posix_spawn_file_actions_init(&fa)) {
         posix_spawnattr_destroy(&attr);
@@ -383,19 +332,19 @@
         return -1;
     }
 
-    if (int ret = posix_spawn_file_actions_adddup2(&fa, tunFd, passedTunFd)) {
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, tunFd, tunFd)) {
         posix_spawnattr_destroy(&attr);
         posix_spawn_file_actions_destroy(&fa);
         throwIOException(env, "posix_spawn_file_actions_adddup2 for tun fd failed", ret);
         return -1;
     }
-    if (int ret = posix_spawn_file_actions_adddup2(&fa, readSock, passedSockRead)) {
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, readSock, readSock)) {
         posix_spawnattr_destroy(&attr);
         posix_spawn_file_actions_destroy(&fa);
         throwIOException(env, "posix_spawn_file_actions_adddup2 for read socket failed", ret);
         return -1;
     }
-    if (int ret = posix_spawn_file_actions_adddup2(&fa, writeSock, passedSockWrite)) {
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, writeSock, writeSock)) {
         posix_spawnattr_destroy(&attr);
         posix_spawn_file_actions_destroy(&fa);
         throwIOException(env, "posix_spawn_file_actions_adddup2 for write socket failed", ret);
@@ -414,15 +363,6 @@
     posix_spawnattr_destroy(&attr);
     posix_spawn_file_actions_destroy(&fa);
 
-    // 5. Start BPF if any
-    if (!net::clat::initMaps()) {
-        net::clat::ClatdTracker tracker = {};
-        if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
-                &tracker)) {
-            net::clat::maybeStartBpf(tracker);
-        }
-    }
-
     return pid;
 }
 
@@ -477,15 +417,73 @@
         return;
     }
 
-    if (!net::clat::initMaps()) {
-        net::clat::ClatdTracker tracker = {};
-        if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
-                &tracker)) {
-            net::clat::maybeStopBpf(tracker);
-        }
+    stopClatdProcess(pid);
+}
+
+static jlong com_android_server_connectivity_ClatCoordinator_tagSocketAsClat(
+        JNIEnv* env, jobject clazz, jobject sockJavaFd) {
+    int sockFd = netjniutils::GetNativeFileDescriptor(env, sockJavaFd);
+    if (sockFd < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid socket file descriptor");
+        return -1;
     }
 
-    stopClatdProcess(pid);
+    uint64_t sock_cookie = bpf::getSocketCookie(sockFd);
+    if (sock_cookie == bpf::NONEXISTENT_COOKIE) {
+        throwIOException(env, "get socket cookie failed", errno);
+        return -1;
+    }
+
+    bpf::BpfMap<uint64_t, UidTagValue> cookieTagMap;
+    auto res = cookieTagMap.init(COOKIE_TAG_MAP_PATH);
+    if (!res.ok()) {
+        throwIOException(env, "failed to init the cookieTagMap", res.error().code());
+        return -1;
+    }
+
+    // Tag raw socket with uid AID_CLAT and set tag as zero because tag is unused in bpf
+    // program for counting data usage in netd.c. Tagging socket is used to avoid counting
+    // duplicated clat traffic in bpf stat.
+    UidTagValue newKey = {.uid = (uint32_t)AID_CLAT, .tag = 0 /* unused */};
+    res = cookieTagMap.writeValue(sock_cookie, newKey, BPF_ANY);
+    if (!res.ok()) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Failed to tag the socket: %s, fd: %d",
+                             strerror(res.error().code()), cookieTagMap.getMap().get());
+        return -1;
+    }
+
+    ALOGI("tag uid AID_CLAT to socket fd %d, cookie %" PRIu64 "", sockFd, sock_cookie);
+    return static_cast<jlong>(sock_cookie);
+}
+
+static void com_android_server_connectivity_ClatCoordinator_untagSocket(JNIEnv* env, jobject clazz,
+                                                                        jlong cookie) {
+    uint64_t sock_cookie = static_cast<uint64_t>(cookie);
+    if (sock_cookie == bpf::NONEXISTENT_COOKIE) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid socket cookie");
+        return;
+    }
+
+    // The reason that deleting entry from cookie tag map directly is that the tag socket destroy
+    // listener only monitors on group INET_TCP, INET_UDP, INET6_TCP, INET6_UDP. The other socket
+    // types, ex: raw, are not able to be removed automatically by the listener.
+    // See TrafficController::makeSkDestroyListener.
+    bpf::BpfMap<uint64_t, UidTagValue> cookieTagMap;
+    auto res = cookieTagMap.init(COOKIE_TAG_MAP_PATH);
+    if (!res.ok()) {
+        throwIOException(env, "failed to init the cookieTagMap", res.error().code());
+        return;
+    }
+
+    res = cookieTagMap.deleteValue(sock_cookie);
+    if (!res.ok()) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Failed to untag the socket: %s",
+                             strerror(res.error().code()));
+        return;
+    }
+
+    ALOGI("untag socket cookie %" PRIu64 "", sock_cookie);
+    return;
 }
 
 /*
@@ -517,6 +515,10 @@
         {"native_stopClatd",
          "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
          (void*)com_android_server_connectivity_ClatCoordinator_stopClatd},
+        {"native_tagSocketAsClat", "(Ljava/io/FileDescriptor;)J",
+         (void*)com_android_server_connectivity_ClatCoordinator_tagSocketAsClat},
+        {"native_untagSocket", "(J)V",
+         (void*)com_android_server_connectivity_ClatCoordinator_untagSocket},
 };
 
 int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env) {
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index facdad7..3d15d43 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -17,11 +17,15 @@
 #include <nativehelper/JNIHelp.h>
 #include <log/log.h>
 
+#include <android-modules-utils/sdk_level.h>
+
 namespace android {
 
 int register_com_android_server_TestNetworkService(JNIEnv* env);
 int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env);
 int register_com_android_server_BpfNetMaps(JNIEnv* env);
+int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
+int register_android_server_net_NetworkStatsService(JNIEnv* env);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
@@ -42,6 +46,16 @@
         return JNI_ERR;
     }
 
+    if (android::modules::sdklevel::IsAtLeastT()) {
+        if (register_android_server_net_NetworkStatsFactory(env) < 0) {
+            return JNI_ERR;
+        }
+
+        if (register_android_server_net_NetworkStatsService(env) < 0) {
+            return JNI_ERR;
+        }
+    }
+
     return JNI_VERSION_1_6;
 }
 
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 1cbfd94..3e98edb 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -65,7 +65,6 @@
 using netdutils::Status;
 using netdutils::statusFromErrno;
 using netdutils::StatusOr;
-using netdutils::status::ok;
 
 constexpr int kSockDiagMsgType = SOCK_DIAG_BY_FAMILY;
 constexpr int kSockDiagDoneMsgType = NLMSG_DONE;
@@ -601,8 +600,10 @@
     }
 }
 
-void TrafficController::dump(DumpWriter& dw, bool verbose) {
+void TrafficController::dump(int fd, bool verbose) {
     std::lock_guard guard(mMutex);
+    DumpWriter dw(fd);
+
     ScopedIndent indentTop(dw);
     dw.println("TrafficController");
 
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index 6fe117f..79e75ac 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -62,7 +62,7 @@
     netdutils::Status updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
                                           FirewallType type) EXCLUDES(mMutex);
 
-    void dump(netdutils::DumpWriter& dw, bool verbose) EXCLUDES(mMutex);
+    void dump(int fd, bool verbose) EXCLUDES(mMutex);
 
     netdutils::Status replaceRulesInMap(UidOwnerMatchType match, const std::vector<int32_t>& uids)
             EXCLUDES(mMutex);
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 17ee996..68e4dc4 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -19,19 +19,12 @@
 cc_library_static {
     name: "libclat",
     defaults: ["netd_defaults"],
-    header_libs: [
-        "bpf_connectivity_headers",
-        "libbase_headers",
-    ],
     srcs: [
-        "TcUtils.cpp",  // TODO: move to frameworks/libs/net
-        "bpfhelper.cpp",
         "clatutils.cpp",
     ],
     stl: "libc++_static",
     static_libs: [
         "libip_checksum",
-        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
     ],
     shared_libs: ["liblog"],
     export_include_dirs: ["include"],
@@ -43,11 +36,7 @@
     name: "libclat_test",
     defaults: ["netd_defaults"],
     test_suites: ["device-tests"],
-    header_libs: [
-        "bpf_connectivity_headers",
-    ],
     srcs: [
-        "TcUtilsTest.cpp",
         "clatutils_test.cpp",
     ],
     static_libs: [
@@ -55,8 +44,6 @@
         "libclat",
         "libip_checksum",
         "libnetd_test_tun_interface",
-        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
-        "libtcutils",
     ],
     shared_libs: [
         "liblog",
diff --git a/service/native/libs/libclat/TcUtils.cpp b/service/native/libs/libclat/TcUtils.cpp
deleted file mode 100644
index cdfb763..0000000
--- a/service/native/libs/libclat/TcUtils.cpp
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "TcUtils"
-
-#include "libclat/TcUtils.h"
-
-#include <arpa/inet.h>
-#include <linux/if.h>
-#include <linux/if_arp.h>
-#include <linux/netlink.h>
-#include <linux/pkt_cls.h>
-#include <linux/pkt_sched.h>
-#include <sys/ioctl.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include <log/log.h>
-
-#include "android-base/unique_fd.h"
-
-namespace android {
-namespace net {
-
-using std::max;
-
-// Sync from system/netd/server/NetlinkCommands.h
-const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
-const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
-
-static int doSIOCGIF(const std::string& interface, int opt) {
-    base::unique_fd ufd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0));
-
-    if (ufd < 0) {
-        const int err = errno;
-        ALOGE("socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0)");
-        return -err;
-    };
-
-    struct ifreq ifr = {};
-    // We use strncpy() instead of strlcpy() since kernel has to be able
-    // to handle non-zero terminated junk passed in by userspace anyway,
-    // and this way too long interface names (more than IFNAMSIZ-1 = 15
-    // characters plus terminating NULL) will not get truncated to 15
-    // characters and zero-terminated and thus potentially erroneously
-    // match a truncated interface if one were to exist.
-    strncpy(ifr.ifr_name, interface.c_str(), sizeof(ifr.ifr_name));
-
-    if (ioctl(ufd, opt, &ifr, sizeof(ifr))) return -errno;
-
-    if (opt == SIOCGIFHWADDR) return ifr.ifr_hwaddr.sa_family;
-    if (opt == SIOCGIFMTU) return ifr.ifr_mtu;
-    return -EINVAL;
-}
-
-int hardwareAddressType(const std::string& interface) {
-    return doSIOCGIF(interface, SIOCGIFHWADDR);
-}
-
-int deviceMTU(const std::string& interface) {
-    return doSIOCGIF(interface, SIOCGIFMTU);
-}
-
-base::Result<bool> isEthernet(const std::string& interface) {
-    int rv = hardwareAddressType(interface);
-    if (rv < 0) {
-        errno = -rv;
-        return ErrnoErrorf("Get hardware address type of interface {} failed", interface);
-    }
-
-    switch (rv) {
-        case ARPHRD_ETHER:
-            return true;
-        case ARPHRD_NONE:
-        case ARPHRD_RAWIP:  // in Linux 4.14+ rmnet support was upstreamed and this is 519
-        case 530:           // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet
-            return false;
-        default:
-            errno = EAFNOSUPPORT;  // Address family not supported
-            return ErrnoErrorf("Unknown hardware address type {} on interface {}", rv, interface);
-    }
-}
-
-// TODO: use //system/netd/server/NetlinkCommands.cpp:openNetlinkSocket(protocol)
-// and //system/netd/server/SockDiag.cpp:checkError(fd)
-static int sendAndProcessNetlinkResponse(const void* req, int len) {
-    base::unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
-    if (fd == -1) {
-        const int err = errno;
-        ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE)");
-        return -err;
-    }
-
-    static constexpr int on = 1;
-    int rv = setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on));
-    if (rv) ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on);
-
-    // this is needed to get sane strace netlink parsing, it allocates the pid
-    rv = bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
-    if (rv) {
-        const int err = errno;
-        ALOGE("bind(fd, {AF_NETLINK, 0, 0})");
-        return -err;
-    }
-
-    // we do not want to receive messages from anyone besides the kernel
-    rv = connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
-    if (rv) {
-        const int err = errno;
-        ALOGE("connect(fd, {AF_NETLINK, 0, 0})");
-        return -err;
-    }
-
-    rv = send(fd, req, len, 0);
-    if (rv == -1) return -errno;
-    if (rv != len) return -EMSGSIZE;
-
-    struct {
-        nlmsghdr h;
-        nlmsgerr e;
-        char buf[256];
-    } resp = {};
-
-    rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
-
-    if (rv == -1) {
-        const int err = errno;
-        ALOGE("recv() failed");
-        return -err;
-    }
-
-    if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
-        ALOGE("recv() returned short packet: %d", rv);
-        return -EMSGSIZE;
-    }
-
-    if (resp.h.nlmsg_len != (unsigned)rv) {
-        ALOGE("recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, rv);
-        return -EBADMSG;
-    }
-
-    if (resp.h.nlmsg_type != NLMSG_ERROR) {
-        ALOGE("recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type);
-        return -EBADMSG;
-    }
-
-    return resp.e.error;  // returns 0 on success
-}
-
-// ADD:     nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_EXCL|NLM_F_CREATE
-// REPLACE: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_CREATE|NLM_F_REPLACE
-// DEL:     nlMsgType=RTM_DELQDISC nlMsgFlags=0
-int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags) {
-    // This is the name of the qdisc we are attaching.
-    // Some hoop jumping to make this compile time constant with known size,
-    // so that the structure declaration is well defined at compile time.
-#define CLSACT "clsact"
-    // sizeof() includes the terminating NULL
-    static constexpr size_t ASCIIZ_LEN_CLSACT = sizeof(CLSACT);
-
-    const struct {
-        nlmsghdr n;
-        tcmsg t;
-        struct {
-            nlattr attr;
-            char str[NLMSG_ALIGN(ASCIIZ_LEN_CLSACT)];
-        } kind;
-    } req = {
-            .n =
-                    {
-                            .nlmsg_len = sizeof(req),
-                            .nlmsg_type = nlMsgType,
-                            .nlmsg_flags = static_cast<__u16>(NETLINK_REQUEST_FLAGS | nlMsgFlags),
-                    },
-            .t =
-                    {
-                            .tcm_family = AF_UNSPEC,
-                            .tcm_ifindex = ifIndex,
-                            .tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0),
-                            .tcm_parent = TC_H_CLSACT,
-                    },
-            .kind =
-                    {
-                            .attr =
-                                    {
-                                            .nla_len = NLA_HDRLEN + ASCIIZ_LEN_CLSACT,
-                                            .nla_type = TCA_KIND,
-                                    },
-                            .str = CLSACT,
-                    },
-    };
-#undef CLSACT
-
-    return sendAndProcessNetlinkResponse(&req, sizeof(req));
-}
-
-// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
-// direct-action
-int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet) {
-    // This is the name of the filter we're attaching (ie. this is the 'bpf'
-    // packet classifier enabled by kernel config option CONFIG_NET_CLS_BPF.
-    //
-    // We go through some hoops in order to make this compile time constants
-    // so that we can define the struct further down the function with the
-    // field for this sized correctly already during the build.
-#define BPF "bpf"
-    // sizeof() includes the terminating NULL
-    static constexpr size_t ASCIIZ_LEN_BPF = sizeof(BPF);
-
-    // This is to replicate program name suffix used by 'tc' Linux cli
-    // when it attaches programs.
-#define FSOBJ_SUFFIX ":[*fsobj]"
-
-    // This macro expands (from header files) to:
-    //   prog_clatd_schedcls_ingress6_clat_rawip:[*fsobj]
-    // and is the name of the pinned ingress ebpf program for ARPHRD_RAWIP interfaces.
-    // (also compatible with anything that has 0 size L2 header)
-    static constexpr char name_clat_rx_rawip[] = CLAT_INGRESS6_PROG_RAWIP_NAME FSOBJ_SUFFIX;
-
-    // This macro expands (from header files) to:
-    //   prog_clatd_schedcls_ingress6_clat_ether:[*fsobj]
-    // and is the name of the pinned ingress ebpf program for ARPHRD_ETHER interfaces.
-    // (also compatible with anything that has standard ethernet header)
-    static constexpr char name_clat_rx_ether[] = CLAT_INGRESS6_PROG_ETHER_NAME FSOBJ_SUFFIX;
-
-    // This macro expands (from header files) to:
-    //   prog_clatd_schedcls_egress4_clat_rawip:[*fsobj]
-    // and is the name of the pinned egress ebpf program for ARPHRD_RAWIP interfaces.
-    // (also compatible with anything that has 0 size L2 header)
-    static constexpr char name_clat_tx_rawip[] = CLAT_EGRESS4_PROG_RAWIP_NAME FSOBJ_SUFFIX;
-
-    // This macro expands (from header files) to:
-    //   prog_clatd_schedcls_egress4_clat_ether:[*fsobj]
-    // and is the name of the pinned egress ebpf program for ARPHRD_ETHER interfaces.
-    // (also compatible with anything that has standard ethernet header)
-    static constexpr char name_clat_tx_ether[] = CLAT_EGRESS4_PROG_ETHER_NAME FSOBJ_SUFFIX;
-
-#undef FSOBJ_SUFFIX
-
-    // The actual name we'll use is determined at run time via 'ethernet' and 'ingress'
-    // booleans.  We need to compile time allocate enough space in the struct
-    // hence this macro magic to make sure we have enough space for either
-    // possibility.  In practice some of these are actually the same size.
-    static constexpr size_t ASCIIZ_MAXLEN_NAME = max({
-            sizeof(name_clat_rx_rawip),
-            sizeof(name_clat_rx_ether),
-            sizeof(name_clat_tx_rawip),
-            sizeof(name_clat_tx_ether),
-    });
-
-    // These are not compile time constants: 'name' is used in strncpy below
-    const char* const name_clat_rx = ethernet ? name_clat_rx_ether : name_clat_rx_rawip;
-    const char* const name_clat_tx = ethernet ? name_clat_tx_ether : name_clat_tx_rawip;
-    const char* const name = ingress ? name_clat_rx : name_clat_tx;
-
-    struct {
-        nlmsghdr n;
-        tcmsg t;
-        struct {
-            nlattr attr;
-            char str[NLMSG_ALIGN(ASCIIZ_LEN_BPF)];
-        } kind;
-        struct {
-            nlattr attr;
-            struct {
-                nlattr attr;
-                __u32 u32;
-            } fd;
-            struct {
-                nlattr attr;
-                char str[NLMSG_ALIGN(ASCIIZ_MAXLEN_NAME)];
-            } name;
-            struct {
-                nlattr attr;
-                __u32 u32;
-            } flags;
-        } options;
-    } req = {
-            .n =
-                    {
-                            .nlmsg_len = sizeof(req),
-                            .nlmsg_type = RTM_NEWTFILTER,
-                            .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
-                    },
-            .t =
-                    {
-                            .tcm_family = AF_UNSPEC,
-                            .tcm_ifindex = ifIndex,
-                            .tcm_handle = TC_H_UNSPEC,
-                            .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
-                                                    ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
-                            .tcm_info = static_cast<__u32>((PRIO_CLAT << 16) | htons(proto)),
-                    },
-            .kind =
-                    {
-                            .attr =
-                                    {
-                                            .nla_len = sizeof(req.kind),
-                                            .nla_type = TCA_KIND,
-                                    },
-                            .str = BPF,
-                    },
-            .options =
-                    {
-                            .attr =
-                                    {
-                                            .nla_len = sizeof(req.options),
-                                            .nla_type = NLA_F_NESTED | TCA_OPTIONS,
-                                    },
-                            .fd =
-                                    {
-                                            .attr =
-                                                    {
-                                                            .nla_len = sizeof(req.options.fd),
-                                                            .nla_type = TCA_BPF_FD,
-                                                    },
-                                            .u32 = static_cast<__u32>(bpfFd),
-                                    },
-                            .name =
-                                    {
-                                            .attr =
-                                                    {
-                                                            .nla_len = sizeof(req.options.name),
-                                                            .nla_type = TCA_BPF_NAME,
-                                                    },
-                                            // Visible via 'tc filter show', but
-                                            // is overwritten by strncpy below
-                                            .str = "placeholder",
-                                    },
-                            .flags =
-                                    {
-                                            .attr =
-                                                    {
-                                                            .nla_len = sizeof(req.options.flags),
-                                                            .nla_type = TCA_BPF_FLAGS,
-                                                    },
-                                            .u32 = TCA_BPF_FLAG_ACT_DIRECT,
-                                    },
-                    },
-    };
-#undef BPF
-
-    strncpy(req.options.name.str, name, sizeof(req.options.name.str));
-
-    return sendAndProcessNetlinkResponse(&req, sizeof(req));
-}
-
-// tc filter del dev .. in/egress prio 4 protocol ..
-int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto) {
-    const struct {
-        nlmsghdr n;
-        tcmsg t;
-    } req = {
-            .n =
-                    {
-                            .nlmsg_len = sizeof(req),
-                            .nlmsg_type = RTM_DELTFILTER,
-                            .nlmsg_flags = NETLINK_REQUEST_FLAGS,
-                    },
-            .t =
-                    {
-                            .tcm_family = AF_UNSPEC,
-                            .tcm_ifindex = ifIndex,
-                            .tcm_handle = TC_H_UNSPEC,
-                            .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
-                                                    ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
-                            .tcm_info = (static_cast<uint32_t>(prio) << 16) |
-                                        static_cast<uint32_t>(htons(proto)),
-                    },
-    };
-
-    return sendAndProcessNetlinkResponse(&req, sizeof(req));
-}
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/TcUtilsTest.cpp b/service/native/libs/libclat/TcUtilsTest.cpp
deleted file mode 100644
index 08f3042..0000000
--- a/service/native/libs/libclat/TcUtilsTest.cpp
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * TcUtilsTest.cpp - unit tests for TcUtils.cpp
- */
-
-#include <gtest/gtest.h>
-
-#include "libclat/TcUtils.h"
-
-#include <linux/if_arp.h>
-#include <stdlib.h>
-#include <sys/wait.h>
-
-#include "bpf/BpfUtils.h"
-#include "bpf_shared.h"
-
-namespace android {
-namespace net {
-
-class TcUtilsTest : public ::testing::Test {
-  public:
-    void SetUp() {}
-};
-
-TEST_F(TcUtilsTest, HardwareAddressTypeOfNonExistingIf) {
-    ASSERT_EQ(-ENODEV, hardwareAddressType("not_existing_if"));
-}
-
-TEST_F(TcUtilsTest, HardwareAddressTypeOfLoopback) {
-    ASSERT_EQ(ARPHRD_LOOPBACK, hardwareAddressType("lo"));
-}
-
-// If wireless 'wlan0' interface exists it should be Ethernet.
-TEST_F(TcUtilsTest, HardwareAddressTypeOfWireless) {
-    int type = hardwareAddressType("wlan0");
-    if (type == -ENODEV) return;
-
-    ASSERT_EQ(ARPHRD_ETHER, type);
-}
-
-// If cellular 'rmnet_data0' interface exists it should
-// *probably* not be Ethernet and instead be RawIp.
-TEST_F(TcUtilsTest, HardwareAddressTypeOfCellular) {
-    int type = hardwareAddressType("rmnet_data0");
-    if (type == -ENODEV) return;
-
-    ASSERT_NE(ARPHRD_ETHER, type);
-
-    // ARPHRD_RAWIP is 530 on some pre-4.14 Qualcomm devices.
-    if (type == 530) return;
-
-    ASSERT_EQ(ARPHRD_RAWIP, type);
-}
-
-TEST_F(TcUtilsTest, IsEthernetOfNonExistingIf) {
-    auto res = isEthernet("not_existing_if");
-    ASSERT_FALSE(res.ok());
-    ASSERT_EQ(ENODEV, res.error().code());
-}
-
-TEST_F(TcUtilsTest, IsEthernetOfLoopback) {
-    auto res = isEthernet("lo");
-    ASSERT_FALSE(res.ok());
-    ASSERT_EQ(EAFNOSUPPORT, res.error().code());
-}
-
-// If wireless 'wlan0' interface exists it should be Ethernet.
-// See also HardwareAddressTypeOfWireless.
-TEST_F(TcUtilsTest, IsEthernetOfWireless) {
-    auto res = isEthernet("wlan0");
-    if (!res.ok() && res.error().code() == ENODEV) return;
-
-    ASSERT_RESULT_OK(res);
-    ASSERT_TRUE(res.value());
-}
-
-// If cellular 'rmnet_data0' interface exists it should
-// *probably* not be Ethernet and instead be RawIp.
-// See also HardwareAddressTypeOfCellular.
-TEST_F(TcUtilsTest, IsEthernetOfCellular) {
-    auto res = isEthernet("rmnet_data0");
-    if (!res.ok() && res.error().code() == ENODEV) return;
-
-    ASSERT_RESULT_OK(res);
-    ASSERT_FALSE(res.value());
-}
-
-TEST_F(TcUtilsTest, DeviceMTUOfNonExistingIf) {
-    ASSERT_EQ(-ENODEV, deviceMTU("not_existing_if"));
-}
-
-TEST_F(TcUtilsTest, DeviceMTUofLoopback) {
-    ASSERT_EQ(65536, deviceMTU("lo"));
-}
-
-TEST_F(TcUtilsTest, GetClatEgress4MapFd) {
-    int fd = getClatEgress4MapFd();
-    ASSERT_GE(fd, 3);  // 0,1,2 - stdin/out/err, thus fd >= 3
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatEgress4RawIpProgFd) {
-    int fd = getClatEgress4ProgFd(RAWIP);
-    ASSERT_GE(fd, 3);
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatEgress4EtherProgFd) {
-    int fd = getClatEgress4ProgFd(ETHER);
-    ASSERT_GE(fd, 3);
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatIngress6MapFd) {
-    int fd = getClatIngress6MapFd();
-    ASSERT_GE(fd, 3);  // 0,1,2 - stdin/out/err, thus fd >= 3
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatIngress6RawIpProgFd) {
-    int fd = getClatIngress6ProgFd(RAWIP);
-    ASSERT_GE(fd, 3);
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatIngress6EtherProgFd) {
-    int fd = getClatIngress6ProgFd(ETHER);
-    ASSERT_GE(fd, 3);
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-// See Linux kernel source in include/net/flow.h
-#define LOOPBACK_IFINDEX 1
-
-TEST_F(TcUtilsTest, AttachReplaceDetachClsactLo) {
-    // This attaches and detaches a configuration-less and thus no-op clsact
-    // qdisc to loopback interface (and it takes fractions of a second)
-    EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(0, tcQdiscReplaceDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-EINVAL, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
-}
-
-static void checkAttachDetachBpfFilterClsactLo(const bool ingress, const bool ethernet) {
-    // Older kernels return EINVAL instead of ENOENT due to lacking proper error propagation...
-    const int errNOENT = android::bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
-
-    int clatBpfFd = ingress ? getClatIngress6ProgFd(ethernet) : getClatEgress4ProgFd(ethernet);
-    ASSERT_GE(clatBpfFd, 3);
-
-    // This attaches and detaches a clsact plus ebpf program to loopback
-    // interface, but it should not affect traffic by virtue of us not
-    // actually populating the ebpf control map.
-    // Furthermore: it only takes fractions of a second.
-    EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-    EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-    if (ingress) {
-        EXPECT_EQ(0, tcFilterAddDevIngressClatIpv6(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
-        EXPECT_EQ(0, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    } else {
-        EXPECT_EQ(0, tcFilterAddDevEgressClatIpv4(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
-        EXPECT_EQ(0, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-    }
-    EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-    EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-
-    close(clatBpfFd);
-}
-
-TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactEgressLo) {
-    checkAttachDetachBpfFilterClsactLo(EGRESS, RAWIP);
-}
-
-TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactEgressLo) {
-    checkAttachDetachBpfFilterClsactLo(EGRESS, ETHER);
-}
-
-TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactIngressLo) {
-    checkAttachDetachBpfFilterClsactLo(INGRESS, RAWIP);
-}
-
-TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactIngressLo) {
-    checkAttachDetachBpfFilterClsactLo(INGRESS, ETHER);
-}
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/bpfhelper.cpp b/service/native/libs/libclat/bpfhelper.cpp
deleted file mode 100644
index 6e230d0..0000000
--- a/service/native/libs/libclat/bpfhelper.cpp
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * main.c - main function
- */
-#define LOG_TAG "bpfhelper"
-
-#include "libclat/bpfhelper.h"
-
-#include <android-base/unique_fd.h>
-#include <log/log.h>
-
-#include "bpf/BpfMap.h"
-#include "libclat/TcUtils.h"
-
-#define DEVICEPREFIX "v4-"
-
-using android::base::unique_fd;
-using android::net::RAWIP;
-using android::net::getClatEgress4MapFd;
-using android::net::getClatIngress6MapFd;
-using android::net::getClatEgress4ProgFd;
-using android::net::getClatIngress6ProgFd;
-using android::net::tcQdiscAddDevClsact;
-using android::net::tcFilterAddDevEgressClatIpv4;
-using android::net::tcFilterAddDevIngressClatIpv6;
-using android::net::tcFilterDelDevEgressClatIpv4;
-using android::net::tcFilterDelDevIngressClatIpv6;
-using android::bpf::BpfMap;
-
-BpfMap<ClatEgress4Key, ClatEgress4Value> mClatEgress4Map;
-BpfMap<ClatIngress6Key, ClatIngress6Value> mClatIngress6Map;
-
-namespace android {
-namespace net {
-namespace clat {
-
-// TODO: have a clearMap function to remove all stubs while system server crash.
-// For long term, move bpf access into java and map initialization should live
-// ClatCoordinator constructor.
-int initMaps(void) {
-    int rv = getClatEgress4MapFd();
-    if (rv < 0) {
-        ALOGE("getClatEgress4MapFd() failure: %s", strerror(-rv));
-        return -rv;
-    }
-    mClatEgress4Map.reset(rv);
-
-    rv = getClatIngress6MapFd();
-    if (rv < 0) {
-        ALOGE("getClatIngress6MapFd() failure: %s", strerror(-rv));
-        mClatEgress4Map.reset(-1);
-        return -rv;
-    }
-    mClatIngress6Map.reset(rv);
-
-    return 0;
-}
-
-void maybeStartBpf(const ClatdTracker& tracker) {
-    auto isEthernet = android::net::isEthernet(tracker.iface);
-    if (!isEthernet.ok()) {
-        ALOGE("isEthernet(%s[%d]) failure: %s", tracker.iface, tracker.ifIndex,
-              isEthernet.error().message().c_str());
-        return;
-    }
-
-    // This program will be attached to the v4-* interface which is a TUN and thus always rawip.
-    int rv = getClatEgress4ProgFd(RAWIP);
-    if (rv < 0) {
-        ALOGE("getClatEgress4ProgFd(RAWIP) failure: %s", strerror(-rv));
-        return;
-    }
-    unique_fd txRawIpProgFd(rv);
-
-    rv = getClatIngress6ProgFd(isEthernet.value());
-    if (rv < 0) {
-        ALOGE("getClatIngress6ProgFd(%d) failure: %s", isEthernet.value(), strerror(-rv));
-        return;
-    }
-    unique_fd rxProgFd(rv);
-
-    ClatEgress4Key txKey = {
-            .iif = tracker.v4ifIndex,
-            .local4 = tracker.v4,
-    };
-    ClatEgress4Value txValue = {
-            .oif = tracker.ifIndex,
-            .local6 = tracker.v6,
-            .pfx96 = tracker.pfx96,
-            .oifIsEthernet = isEthernet.value(),
-    };
-
-    auto ret = mClatEgress4Map.writeValue(txKey, txValue, BPF_ANY);
-    if (!ret.ok()) {
-        ALOGE("mClatEgress4Map.writeValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    ClatIngress6Key rxKey = {
-            .iif = tracker.ifIndex,
-            .pfx96 = tracker.pfx96,
-            .local6 = tracker.v6,
-    };
-    ClatIngress6Value rxValue = {
-            // TODO: move all the clat code to eBPF and remove the tun interface entirely.
-            .oif = tracker.v4ifIndex,
-            .local4 = tracker.v4,
-    };
-
-    ret = mClatIngress6Map.writeValue(rxKey, rxValue, BPF_ANY);
-    if (!ret.ok()) {
-        ALOGE("mClatIngress6Map.writeValue failure: %s", strerror(ret.error().code()));
-        ret = mClatEgress4Map.deleteValue(txKey);
-        if (!ret.ok())
-            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    // We do tc setup *after* populating the maps, so scanning through them
-    // can always be used to tell us what needs cleanup.
-
-    // Usually the clsact will be added in RouteController::addInterfaceToPhysicalNetwork.
-    // But clat is started before the v4- interface is added to the network. The clat startup have
-    // to add clsact of v4- tun interface first for adding bpf filter in maybeStartBpf.
-    // TODO: move "qdisc add clsact" of v4- tun interface out from ClatdController.
-    rv = tcQdiscAddDevClsact(tracker.v4ifIndex);
-    if (rv) {
-        ALOGE("tcQdiscAddDevClsact(%d[%s]) failure: %s", tracker.v4ifIndex, tracker.v4iface,
-              strerror(-rv));
-        ret = mClatEgress4Map.deleteValue(txKey);
-        if (!ret.ok())
-            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-        ret = mClatIngress6Map.deleteValue(rxKey);
-        if (!ret.ok())
-            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    rv = tcFilterAddDevEgressClatIpv4(tracker.v4ifIndex, txRawIpProgFd, RAWIP);
-    if (rv) {
-        ALOGE("tcFilterAddDevEgressClatIpv4(%d[%s], RAWIP) failure: %s", tracker.v4ifIndex,
-              tracker.v4iface, strerror(-rv));
-
-        // The v4- interface clsact is not deleted for unwinding error because once it is created
-        // with interface addition, the lifetime is till interface deletion. Moreover, the clsact
-        // has no clat filter now. It should not break anything.
-
-        ret = mClatEgress4Map.deleteValue(txKey);
-        if (!ret.ok())
-            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-        ret = mClatIngress6Map.deleteValue(rxKey);
-        if (!ret.ok())
-            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    rv = tcFilterAddDevIngressClatIpv6(tracker.ifIndex, rxProgFd, isEthernet.value());
-    if (rv) {
-        ALOGE("tcFilterAddDevIngressClatIpv6(%d[%s], %d) failure: %s", tracker.ifIndex,
-              tracker.iface, isEthernet.value(), strerror(-rv));
-        rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
-        if (rv) {
-            ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
-                  tracker.v4iface, strerror(-rv));
-        }
-
-        // The v4- interface clsact is not deleted. See the reason in the error unwinding code of
-        // the egress filter attaching of v4- tun interface.
-
-        ret = mClatEgress4Map.deleteValue(txKey);
-        if (!ret.ok())
-            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-        ret = mClatIngress6Map.deleteValue(rxKey);
-        if (!ret.ok())
-            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    // success
-}
-
-void maybeStopBpf(const ClatdTracker& tracker) {
-    int rv = tcFilterDelDevIngressClatIpv6(tracker.ifIndex);
-    if (rv < 0) {
-        ALOGE("tcFilterDelDevIngressClatIpv6(%d[%s]) failure: %s", tracker.ifIndex, tracker.iface,
-              strerror(-rv));
-    }
-
-    rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
-    if (rv < 0) {
-        ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
-              tracker.v4iface, strerror(-rv));
-    }
-
-    // We cleanup the maps last, so scanning through them can be used to
-    // determine what still needs cleanup.
-
-    ClatEgress4Key txKey = {
-            .iif = tracker.v4ifIndex,
-            .local4 = tracker.v4,
-    };
-
-    auto ret = mClatEgress4Map.deleteValue(txKey);
-    if (!ret.ok()) ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-
-    ClatIngress6Key rxKey = {
-            .iif = tracker.ifIndex,
-            .pfx96 = tracker.pfx96,
-            .local6 = tracker.v6,
-    };
-
-    ret = mClatIngress6Map.deleteValue(rxKey);
-    if (!ret.ok()) ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
-}
-
-}  // namespace clat
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/TcUtils.h b/service/native/libs/libclat/include/libclat/TcUtils.h
deleted file mode 100644
index 212838e..0000000
--- a/service/native/libs/libclat/include/libclat/TcUtils.h
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <android-base/result.h>
-#include <errno.h>
-#include <linux/if_ether.h>
-#include <linux/if_link.h>
-#include <linux/rtnetlink.h>
-
-#include <string>
-
-#include "bpf/BpfUtils.h"
-#include "bpf_shared.h"
-
-namespace android {
-namespace net {
-
-// For better code clarity - do not change values - used for booleans like
-// with_ethernet_header or isEthernet.
-constexpr bool RAWIP = false;
-constexpr bool ETHER = true;
-
-// For better code clarity when used for 'bool ingress' parameter.
-constexpr bool EGRESS = false;
-constexpr bool INGRESS = true;
-
-// The priority of clat hook - must be after tethering.
-constexpr uint16_t PRIO_CLAT = 4;
-
-// this returns an ARPHRD_* constant or a -errno
-int hardwareAddressType(const std::string& interface);
-
-// return MTU or -errno
-int deviceMTU(const std::string& interface);
-
-base::Result<bool> isEthernet(const std::string& interface);
-
-inline int getClatEgress4MapFd(void) {
-    const int fd = bpf::mapRetrieveRW(CLAT_EGRESS4_MAP_PATH);
-    return (fd == -1) ? -errno : fd;
-}
-
-inline int getClatEgress4ProgFd(bool with_ethernet_header) {
-    const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_EGRESS4_PROG_ETHER_PATH
-                                                             : CLAT_EGRESS4_PROG_RAWIP_PATH);
-    return (fd == -1) ? -errno : fd;
-}
-
-inline int getClatIngress6MapFd(void) {
-    const int fd = bpf::mapRetrieveRW(CLAT_INGRESS6_MAP_PATH);
-    return (fd == -1) ? -errno : fd;
-}
-
-inline int getClatIngress6ProgFd(bool with_ethernet_header) {
-    const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_INGRESS6_PROG_ETHER_PATH
-                                                             : CLAT_INGRESS6_PROG_RAWIP_PATH);
-    return (fd == -1) ? -errno : fd;
-}
-
-int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags);
-
-inline int tcQdiscAddDevClsact(int ifIndex) {
-    return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE);
-}
-
-inline int tcQdiscReplaceDevClsact(int ifIndex) {
-    return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE);
-}
-
-inline int tcQdiscDelDevClsact(int ifIndex) {
-    return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0);
-}
-
-// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
-// direct-action
-int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet);
-
-// tc filter add dev .. ingress prio 4 protocol ipv6 bpf object-pinned /sys/fs/bpf/... direct-action
-inline int tcFilterAddDevIngressClatIpv6(int ifIndex, int bpfFd, bool ethernet) {
-    return tcFilterAddDevBpf(ifIndex, INGRESS, ETH_P_IPV6, bpfFd, ethernet);
-}
-
-// tc filter add dev .. egress prio 4 protocol ip bpf object-pinned /sys/fs/bpf/... direct-action
-inline int tcFilterAddDevEgressClatIpv4(int ifIndex, int bpfFd, bool ethernet) {
-    return tcFilterAddDevBpf(ifIndex, EGRESS, ETH_P_IP, bpfFd, ethernet);
-}
-
-// tc filter del dev .. in/egress prio .. protocol ..
-int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto);
-
-// tc filter del dev .. ingress prio 4 protocol ipv6
-inline int tcFilterDelDevIngressClatIpv6(int ifIndex) {
-    return tcFilterDelDev(ifIndex, INGRESS, PRIO_CLAT, ETH_P_IPV6);
-}
-
-// tc filter del dev .. egress prio 4 protocol ip
-inline int tcFilterDelDevEgressClatIpv4(int ifIndex) {
-    return tcFilterDelDev(ifIndex, EGRESS, PRIO_CLAT, ETH_P_IP);
-}
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/bpfhelper.h b/service/native/libs/libclat/include/libclat/bpfhelper.h
deleted file mode 100644
index c0328c0..0000000
--- a/service/native/libs/libclat/include/libclat/bpfhelper.h
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#pragma once
-
-#include <arpa/inet.h>
-#include <linux/if.h>
-
-namespace android {
-namespace net {
-namespace clat {
-
-struct ClatdTracker {
-    unsigned ifIndex;
-    char iface[IFNAMSIZ];
-    unsigned v4ifIndex;
-    char v4iface[IFNAMSIZ];
-    in_addr v4;
-    in6_addr v6;
-    in6_addr pfx96;
-};
-
-int initMaps(void);
-void maybeStartBpf(const ClatdTracker& tracker);
-void maybeStopBpf(const ClatdTracker& tracker);
-
-}  // namespace clat
-}  // namespace net
-}  // namespace android
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index ddee275..c006bc6 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -16,6 +16,8 @@
 
 package com.android.server;
 
+import static android.system.OsConstants.EOPNOTSUPP;
+
 import android.net.INetd;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
@@ -24,6 +26,9 @@
 
 import com.android.modules.utils.build.SdkLevel;
 
+import java.io.FileDescriptor;
+import java.io.IOException;
+
 /**
  * BpfNetMaps is responsible for providing traffic controller relevant functionality.
  *
@@ -49,6 +54,13 @@
         sInitialized = true;
     }
 
+    /** Constructor used after T that doesn't need to use netd anymore. */
+    public BpfNetMaps() {
+        this(null);
+
+        if (USE_NETD) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
+    }
+
     public BpfNetMaps(INetd netd) {
         ensureInitialized();
         mNetd = netd;
@@ -64,15 +76,10 @@
      * Add naughty app bandwidth rule for specific app
      *
      * @param uid uid of target app
-     * @throws RemoteException when netd has crashed.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
-    public void addNaughtyApp(final int uid) throws RemoteException {
-        if (USE_NETD) {
-            mNetd.bandwidthAddNaughtyApp(uid);
-            return;
-        }
+    public void addNaughtyApp(final int uid) {
         final int err = native_addNaughtyApp(uid);
         maybeThrow(err, "Unable to add naughty app");
     }
@@ -81,15 +88,10 @@
      * Remove naughty app bandwidth rule for specific app
      *
      * @param uid uid of target app
-     * @throws RemoteException when netd has crashed.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
-    public void removeNaughtyApp(final int uid) throws RemoteException {
-        if (USE_NETD) {
-            mNetd.bandwidthRemoveNaughtyApp(uid);
-            return;
-        }
+    public void removeNaughtyApp(final int uid) {
         final int err = native_removeNaughtyApp(uid);
         maybeThrow(err, "Unable to remove naughty app");
     }
@@ -98,15 +100,10 @@
      * Add nice app bandwidth rule for specific app
      *
      * @param uid uid of target app
-     * @throws RemoteException when netd has crashed.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
-    public void addNiceApp(final int uid) throws RemoteException {
-        if (USE_NETD) {
-            mNetd.bandwidthAddNiceApp(uid);
-            return;
-        }
+    public void addNiceApp(final int uid) {
         final int err = native_addNiceApp(uid);
         maybeThrow(err, "Unable to add nice app");
     }
@@ -115,15 +112,10 @@
      * Remove nice app bandwidth rule for specific app
      *
      * @param uid uid of target app
-     * @throws RemoteException when netd has crashed.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
-    public void removeNiceApp(final int uid) throws RemoteException {
-        if (USE_NETD) {
-            mNetd.bandwidthRemoveNiceApp(uid);
-            return;
-        }
+    public void removeNiceApp(final int uid) {
         final int err = native_removeNiceApp(uid);
         maybeThrow(err, "Unable to remove nice app");
     }
@@ -133,15 +125,10 @@
      *
      * @param childChain target chain to enable
      * @param enable     whether to enable or disable child chain.
-     * @throws RemoteException when netd has crashed.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
-    public void setChildChain(final int childChain, final boolean enable) throws RemoteException {
-        if (USE_NETD) {
-            mNetd.firewallEnableChildChain(childChain, enable);
-            return;
-        }
+    public void setChildChain(final int childChain, final boolean enable) {
         final int err = native_setChildChain(childChain, enable);
         maybeThrow(err, "Unable to set child chain");
     }
@@ -158,14 +145,9 @@
      * @param isAllowlist Whether this is an allowlist or denylist chain.
      * @param uids        The list of UIDs to allow/deny.
      * @return 0 if the chain was successfully replaced, errno otherwise.
-     * @throws RemoteException when netd has crashed.
      */
     public int replaceUidChain(final String chainName, final boolean isAllowlist,
-            final int[] uids) throws RemoteException {
-        if (USE_NETD) {
-            mNetd.firewallReplaceUidChain(chainName, isAllowlist, uids);
-            return 0;
-        }
+            final int[] uids) {
         final int err = native_replaceUidChain(chainName, isAllowlist, uids);
         if (err != 0) {
             Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
@@ -179,16 +161,10 @@
      * @param childChain   target chain
      * @param uid          uid to allow/deny
      * @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
-     * @throws RemoteException when netd has crashed.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
-    public void setUidRule(final int childChain, final int uid, final int firewallRule)
-            throws RemoteException {
-        if (USE_NETD) {
-            mNetd.firewallSetUidRule(childChain, uid, firewallRule);
-            return;
-        }
+    public void setUidRule(final int childChain, final int uid, final int firewallRule) {
         final int err = native_setUidRule(childChain, uid, firewallRule);
         maybeThrow(err, "Unable to set uid rule");
     }
@@ -242,15 +218,10 @@
     /**
      * Request netd to change the current active network stats map.
      *
-     * @throws RemoteException when netd has crashed.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
-    public void swapActiveStatsMap() throws RemoteException {
-        if (USE_NETD) {
-            mNetd.trafficSwapActiveStatsMap();
-            return;
-        }
+    public void swapActiveStatsMap() {
         final int err = native_swapActiveStatsMap();
         maybeThrow(err, "Unable to swap active stats map");
     }
@@ -273,6 +244,23 @@
         native_setPermissionForUids(permissions, uids);
     }
 
+    /**
+     * Dump BPF maps
+     *
+     * @param fd file descriptor to output
+     * @throws IOException when file descriptor is invalid.
+     * @throws ServiceSpecificException when the method is called on an unsupported device.
+     */
+    public void dump(final FileDescriptor fd, boolean verbose)
+            throws IOException, ServiceSpecificException {
+        if (USE_NETD) {
+            throw new ServiceSpecificException(
+                    EOPNOTSUPP, "dumpsys connectivity trafficcontroller dump not available on pre-T"
+                    + " devices, use dumpsys netd trafficcontroller instead.");
+        }
+        native_dump(fd, verbose);
+    }
+
     private static native void native_init();
     private native int native_addNaughtyApp(int uid);
     private native int native_removeNaughtyApp(int uid);
@@ -285,4 +273,5 @@
     private native int native_removeUidInterfaceRules(int[] uids);
     private native int native_swapActiveStatsMap();
     private native void native_setPermissionForUids(int permissions, int[] uids);
+    private native void native_dump(FileDescriptor fd, boolean verbose);
 }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 6024a2a..a0e75ec 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -34,6 +34,9 @@
 import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
@@ -198,8 +201,8 @@
 import android.net.resolv.aidl.Nat64PrefixEventParcel;
 import android.net.resolv.aidl.PrivateDnsValidationEventParcel;
 import android.net.shared.PrivateDnsConfig;
-import android.net.util.InterfaceParams;
 import android.net.util.MultinetworkPolicyTracker;
+import android.net.wifi.WifiInfo;
 import android.os.BatteryStatsManager;
 import android.os.Binder;
 import android.os.Build;
@@ -245,6 +248,7 @@
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
 import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
 import com.android.net.module.util.LocationPermissionChecker;
@@ -313,6 +317,7 @@
     public static final String SHORT_ARG = "--short";
     private static final String NETWORK_ARG = "networks";
     private static final String REQUEST_ARG = "requests";
+    private static final String TRAFFICCONTROLLER_ARG = "trafficcontroller";
 
     private static final boolean DBG = true;
     private static final boolean DDBG = Log.isLoggable(TAG, Log.DEBUG);
@@ -347,6 +352,9 @@
     private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
     private static final int DEFAULT_NASCENT_DELAY_MS = 5_000;
 
+    // The maximum value for the blocking validation result, in milliseconds.
+    public static final int MAX_VALIDATION_FAILURE_BLOCKING_TIME_MS = 10000;
+
     // The maximum number of network request allowed per uid before an exception is thrown.
     @VisibleForTesting
     static final int MAX_NETWORK_REQUESTS_PER_UID = 100;
@@ -645,8 +653,9 @@
      * Event for NetworkMonitor to inform ConnectivityService that the probe status has changed.
      * Both of the arguments are bitmasks, and the value of bits come from
      * INetworkMonitor.NETWORK_VALIDATION_PROBE_*.
-     * arg1 = A bitmask to describe which probes are completed.
-     * arg2 = A bitmask to describe which probes are successful.
+     * arg1 = unused
+     * arg2 = netId
+     * obj = A Pair of integers: the bitmasks of, respectively, completed and successful probes.
      */
     public static final int EVENT_PROBE_STATUS_CHANGED = 45;
 
@@ -934,7 +943,7 @@
             }
             // Ethernet is often not specified in the configs, although many devices can use it via
             // USB host adapters. Add it as long as the ethernet service is here.
-            if (ctx.getSystemService(Context.ETHERNET_SERVICE) != null) {
+            if (deviceSupportsEthernet(ctx)) {
                 addSupportedType(TYPE_ETHERNET);
             }
 
@@ -1409,6 +1418,8 @@
                 // converting rateInBytesPerSecond from long to int is safe here because the
                 // setting's range is limited to INT_MAX.
                 // TODO: add long/uint64 support to tcFilterAddDevIngressPolice.
+                Log.i(TAG,
+                        "enableIngressRateLimit on " + iface + ": " + rateInBytesPerSecond + "B/s");
                 TcUtils.tcFilterAddDevIngressPolice(params.index, TC_PRIO_POLICE, (short) ETH_P_ALL,
                         (int) rateInBytesPerSecond, TC_POLICE_BPF_PROG_PATH);
             } catch (IOException e) {
@@ -1430,6 +1441,8 @@
                 return;
             }
             try {
+                Log.i(TAG,
+                        "disableIngressRateLimit on " + iface);
                 TcUtils.tcFilterDelDev(params.index, true, TC_PRIO_POLICE, (short) ETH_P_ALL);
             } catch (IOException e) {
                 loge("TcUtils.tcFilterDelDev(ifaceIndex=" + params.index
@@ -1615,6 +1628,15 @@
                 mContext);
     }
 
+    /**
+     * Check whether or not the device supports Ethernet transport.
+     */
+    public static boolean deviceSupportsEthernet(final Context context) {
+        final PackageManager pm = context.getPackageManager();
+        return pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET)
+                || pm.hasSystemFeature(PackageManager.FEATURE_USB_HOST);
+    }
+
     private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
         return createDefaultNetworkCapabilitiesForUidRangeSet(Collections.singleton(
                 new UidRange(uid, uid)));
@@ -1752,7 +1774,7 @@
 
         // Watch for ingress rate limit changes.
         mSettingsObserver.observe(
-                Settings.Secure.getUriFor(
+                Settings.Global.getUriFor(
                         ConnectivitySettingsManager.INGRESS_RATE_LIMIT_BYTES_PER_SECOND),
                 EVENT_INGRESS_RATE_LIMIT_CHANGED);
     }
@@ -2166,7 +2188,7 @@
 
     @Override
     @Nullable
-    public LinkProperties redactLinkPropertiesForPackage(@NonNull LinkProperties lp, int uid,
+    public LinkProperties getRedactedLinkPropertiesForPackage(@NonNull LinkProperties lp, int uid,
             @NonNull String packageName, @Nullable String callingAttributionTag) {
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(lp);
@@ -2201,8 +2223,9 @@
     }
 
     @Override
-    public NetworkCapabilities redactNetworkCapabilitiesForPackage(@NonNull NetworkCapabilities nc,
-            int uid, @NonNull String packageName, @Nullable String callingAttributionTag) {
+    public NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(
+            @NonNull NetworkCapabilities nc, int uid, @NonNull String packageName,
+            @Nullable String callingAttributionTag) {
         Objects.requireNonNull(nc);
         Objects.requireNonNull(packageName);
         enforceNetworkStackOrSettingsPermission();
@@ -2215,6 +2238,13 @@
                 callingAttributionTag);
     }
 
+    private void redactUnderlyingNetworksForCapabilities(NetworkCapabilities nc, int pid, int uid) {
+        if (nc.getUnderlyingNetworks() != null
+                && !checkNetworkFactoryOrSettingsPermission(pid, uid)) {
+            nc.setUnderlyingNetworks(null);
+        }
+    }
+
     @VisibleForTesting
     NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
             NetworkCapabilities nc, int callerPid, int callerUid) {
@@ -2227,18 +2257,20 @@
         if (!checkSettingsPermission(callerPid, callerUid)) {
             newNc.setUids(null);
             newNc.setSSID(null);
-            // TODO: Processes holding NETWORK_FACTORY should be able to see the underlying networks
-            newNc.setUnderlyingNetworks(null);
         }
         if (newNc.getNetworkSpecifier() != null) {
             newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
         }
-        newNc.setAdministratorUids(new int[0]);
+        if (!checkAnyPermissionOf(callerPid, callerUid, android.Manifest.permission.NETWORK_STACK,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)) {
+            newNc.setAdministratorUids(new int[0]);
+        }
         if (!checkAnyPermissionOf(
                 callerPid, callerUid, android.Manifest.permission.NETWORK_FACTORY)) {
-            newNc.setAccessUids(new ArraySet<>());
+            newNc.setAllowedUids(new ArraySet<>());
             newNc.setSubscriptionIds(Collections.emptySet());
         }
+        redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid);
 
         return newNc;
     }
@@ -2832,12 +2864,16 @@
     }
 
     private void enforceNetworkFactoryPermission() {
+        // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
+        if (getCallingUid() == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.NETWORK_FACTORY,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
     private void enforceNetworkFactoryOrSettingsPermission() {
+        // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
+        if (getCallingUid() == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.NETWORK_SETTINGS,
                 android.Manifest.permission.NETWORK_FACTORY,
@@ -2845,12 +2881,24 @@
     }
 
     private void enforceNetworkFactoryOrTestNetworksPermission() {
+        // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
+        if (getCallingUid() == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.MANAGE_TEST_NETWORKS,
                 android.Manifest.permission.NETWORK_FACTORY,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
+    private boolean checkNetworkFactoryOrSettingsPermission(int pid, int uid) {
+        return PERMISSION_GRANTED == mContext.checkPermission(
+                android.Manifest.permission.NETWORK_FACTORY, pid, uid)
+                || PERMISSION_GRANTED == mContext.checkPermission(
+                android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
+                || PERMISSION_GRANTED == mContext.checkPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid)
+                || uid == Process.BLUETOOTH_UID;
+    }
+
     private boolean checkSettingsPermission() {
         return checkAnyPermissionOf(
                 android.Manifest.permission.NETWORK_SETTINGS,
@@ -3212,6 +3260,10 @@
         } else if (CollectionUtils.contains(args, REQUEST_ARG)) {
             dumpNetworkRequests(pw);
             return;
+        } else if (CollectionUtils.contains(args, TRAFFICCONTROLLER_ARG)) {
+            boolean verbose = !CollectionUtils.contains(args, SHORT_ARG);
+            dumpTrafficController(pw, fd, verbose);
+            return;
         }
 
         pw.print("NetworkProviders for:");
@@ -3429,6 +3481,17 @@
         }
     }
 
+    private void dumpTrafficController(IndentingPrintWriter pw, final FileDescriptor fd,
+            boolean verbose) {
+        try {
+            mBpfNetMaps.dump(fd, verbose);
+        } catch (ServiceSpecificException e) {
+            pw.println(e.getMessage());
+        } catch (IOException e) {
+            loge("Dump BPF maps failed, " + e);
+        }
+    }
+
     private void dumpAllRequestInfoLogsToLogcat() {
         try (PrintWriter logPw = new PrintWriter(new Writer() {
             @Override
@@ -3481,6 +3544,12 @@
         return false;
     }
 
+    private boolean isDisconnectRequest(Message msg) {
+        if (msg.what != NetworkAgent.EVENT_NETWORK_INFO_CHANGED) return false;
+        final NetworkInfo info = (NetworkInfo) ((Pair) msg.obj).second;
+        return info.getState() == NetworkInfo.State.DISCONNECTED;
+    }
+
     // must be stateless - things change under us.
     private class NetworkStateTrackerHandler extends Handler {
         public NetworkStateTrackerHandler(Looper looper) {
@@ -3497,10 +3566,16 @@
                 return;
             }
 
+            // If the network has been destroyed, the only thing that it can do is disconnect.
+            if (nai.destroyed && !isDisconnectRequest(msg)) {
+                return;
+            }
+
             switch (msg.what) {
                 case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
                     final NetworkCapabilities networkCapabilities = new NetworkCapabilities(
                             (NetworkCapabilities) arg.second);
+                    maybeUpdateWifiRoamTimestamp(nai, networkCapabilities);
                     processCapabilitiesFromAgent(nai, networkCapabilities);
                     updateCapabilities(nai.getCurrentScore(), nai, networkCapabilities);
                     break;
@@ -3594,27 +3669,77 @@
                 }
                 case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: {
                     if (mDscpPolicyTracker != null) {
-                        mDscpPolicyTracker.removeAllDscpPolicies(nai);
+                        mDscpPolicyTracker.removeAllDscpPolicies(nai, true);
                     }
                     break;
                 }
+                case NetworkAgent.EVENT_UNREGISTER_AFTER_REPLACEMENT: {
+                    // If nai is not yet created, or is already destroyed, ignore.
+                    if (!shouldDestroyNativeNetwork(nai)) break;
+
+                    final int timeoutMs = (int) arg.second;
+                    if (timeoutMs < 0 || timeoutMs > NetworkAgent.MAX_TEARDOWN_DELAY_MS) {
+                        Log.e(TAG, "Invalid network replacement timer " + timeoutMs
+                                + ", must be between 0 and " + NetworkAgent.MAX_TEARDOWN_DELAY_MS);
+                    }
+
+                    // Marking a network awaiting replacement is used to ensure that any requests
+                    // satisfied by the network do not switch to another network until a
+                    // replacement is available or the wait for a replacement times out.
+                    // If the network is inactive (i.e., nascent or lingering), then there are no
+                    // such requests, and there is no point keeping it. Just tear it down.
+                    // Note that setLingerDuration(0) cannot be used to do this because the network
+                    // could be nascent.
+                    nai.clearInactivityState();
+                    if (unneeded(nai, UnneededFor.TEARDOWN)) {
+                        Log.d(TAG, nai.toShortString()
+                                + " marked awaiting replacement is unneeded, tearing down instead");
+                        teardownUnneededNetwork(nai);
+                        break;
+                    }
+
+                    Log.d(TAG, "Marking " + nai.toShortString()
+                            + " destroyed, awaiting replacement within " + timeoutMs + "ms");
+                    destroyNativeNetwork(nai);
+
+                    // TODO: deduplicate this call with the one in disconnectAndDestroyNetwork.
+                    // This is not trivial because KeepaliveTracker#handleStartKeepalive does not
+                    // consider the fact that the network could already have disconnected or been
+                    // destroyed. Fix the code to send ERROR_INVALID_NETWORK when this happens
+                    // (taking care to ensure no dup'd FD leaks), then remove the code duplication
+                    // and move this code to a sensible location (destroyNativeNetwork perhaps?).
+                    mKeepaliveTracker.handleStopAllKeepalives(nai,
+                            SocketKeepalive.ERROR_INVALID_NETWORK);
+
+                    nai.updateScoreForNetworkAgentUpdate();
+                    // This rematch is almost certainly not going to result in any changes, because
+                    // the destroyed flag is only just above the "current satisfier wins"
+                    // tie-breaker. But technically anything that affects scoring should rematch.
+                    rematchAllNetworksAndRequests();
+                    mHandler.postDelayed(() -> nai.disconnect(), timeoutMs);
+                    break;
+                }
             }
         }
 
         private boolean maybeHandleNetworkMonitorMessage(Message msg) {
+            final int netId = msg.arg2;
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+            // If a network has already been destroyed, all NetworkMonitor updates are ignored.
+            if (nai != null && nai.destroyed) return true;
             switch (msg.what) {
                 default:
                     return false;
                 case EVENT_PROBE_STATUS_CHANGED: {
-                    final Integer netId = (Integer) msg.obj;
-                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
                     if (nai == null) {
                         break;
                     }
+                    final int probesCompleted = ((Pair<Integer, Integer>) msg.obj).first;
+                    final int probesSucceeded = ((Pair<Integer, Integer>) msg.obj).second;
                     final boolean probePrivateDnsCompleted =
-                            ((msg.arg1 & NETWORK_VALIDATION_PROBE_PRIVDNS) != 0);
+                            ((probesCompleted & NETWORK_VALIDATION_PROBE_PRIVDNS) != 0);
                     final boolean privateDnsBroken =
-                            ((msg.arg2 & NETWORK_VALIDATION_PROBE_PRIVDNS) == 0);
+                            ((probesSucceeded & NETWORK_VALIDATION_PROBE_PRIVDNS) == 0);
                     if (probePrivateDnsCompleted) {
                         if (nai.networkCapabilities.isPrivateDnsBroken() != privateDnsBroken) {
                             nai.networkCapabilities.setPrivateDnsBroken(privateDnsBroken);
@@ -3641,7 +3766,6 @@
                 case EVENT_NETWORK_TESTED: {
                     final NetworkTestedResults results = (NetworkTestedResults) msg.obj;
 
-                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(results.mNetId);
                     if (nai == null) break;
 
                     handleNetworkTested(nai, results.mTestResult,
@@ -3649,9 +3773,7 @@
                     break;
                 }
                 case EVENT_PROVISIONING_NOTIFICATION: {
-                    final int netId = msg.arg2;
                     final boolean visible = toBool(msg.arg1);
-                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
                     // If captive portal status has changed, update capabilities or disconnect.
                     if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
                         nai.lastCaptivePortalDetected = visible;
@@ -3685,14 +3807,12 @@
                     break;
                 }
                 case EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
-                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
                     if (nai == null) break;
 
                     updatePrivateDns(nai, (PrivateDnsConfig) msg.obj);
                     break;
                 }
                 case EVENT_CAPPORT_DATA_CHANGED: {
-                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
                     if (nai == null) break;
                     handleCapportApiDataUpdate(nai, (CaptivePortalData) msg.obj);
                     break;
@@ -3703,15 +3823,22 @@
 
         private void handleNetworkTested(
                 @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
+            final boolean valid = ((testResult & NETWORK_VALIDATION_RESULT_VALID) != 0);
+            if (!valid && shouldIgnoreValidationFailureAfterRoam(nai)) {
+                // Assume the validation failure is due to a temporary failure after roaming
+                // and ignore it. NetworkMonitor will continue to retry validation. If it
+                // continues to fail after the block timeout expires, the network will be
+                // marked unvalidated. If it succeeds, then validation state will not change.
+                return;
+            }
+
+            final boolean wasValidated = nai.lastValidated;
+            final boolean wasDefault = isDefaultNetwork(nai);
             final boolean wasPartial = nai.partialConnectivity;
             nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
             final boolean partialConnectivityChanged =
                     (wasPartial != nai.partialConnectivity);
 
-            final boolean valid = ((testResult & NETWORK_VALIDATION_RESULT_VALID) != 0);
-            final boolean wasValidated = nai.lastValidated;
-            final boolean wasDefault = isDefaultNetwork(nai);
-
             if (DBG) {
                 final String logMsg = !TextUtils.isEmpty(redirectUrl)
                         ? " with redirect to " + redirectUrl
@@ -3832,6 +3959,7 @@
             // the same looper so messages will be processed in sequence.
             final Message msg = mTrackerHandler.obtainMessage(
                     EVENT_NETWORK_TESTED,
+                    0, mNetId,
                     new NetworkTestedResults(
                             mNetId, p.result, p.timestampMillis, p.redirectUrl));
             mTrackerHandler.sendMessage(msg);
@@ -3854,7 +3982,7 @@
             ConnectivityReportEvent reportEvent =
                     new ConnectivityReportEvent(p.timestampMillis, nai, extras);
             final Message m = mConnectivityDiagnosticsHandler.obtainMessage(
-                    ConnectivityDiagnosticsHandler.EVENT_NETWORK_TESTED, reportEvent);
+                    ConnectivityDiagnosticsHandler.CMD_SEND_CONNECTIVITY_REPORT, reportEvent);
             mConnectivityDiagnosticsHandler.sendMessage(m);
         }
 
@@ -3869,7 +3997,7 @@
         public void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) {
             mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
                     EVENT_PROBE_STATUS_CHANGED,
-                    probesCompleted, probesSucceeded, new Integer(mNetId)));
+                    0, mNetId, new Pair<>(probesCompleted, probesSucceeded)));
         }
 
         @Override
@@ -4105,6 +4233,27 @@
         }
     }
 
+    private static boolean shouldDestroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
+        return nai.created && !nai.destroyed;
+    }
+
+    private boolean shouldIgnoreValidationFailureAfterRoam(NetworkAgentInfo nai) {
+        // T+ devices should use unregisterAfterReplacement.
+        if (SdkLevel.isAtLeastT()) return false;
+        final long blockTimeOut = Long.valueOf(mResources.get().getInteger(
+                R.integer.config_validationFailureAfterRoamIgnoreTimeMillis));
+        if (blockTimeOut <= MAX_VALIDATION_FAILURE_BLOCKING_TIME_MS
+                && blockTimeOut >= 0) {
+            final long currentTimeMs  = SystemClock.elapsedRealtime();
+            long timeSinceLastRoam = currentTimeMs - nai.lastRoamTimestamp;
+            if (timeSinceLastRoam <= blockTimeOut) {
+                log ("blocked because only " + timeSinceLastRoam + "ms after roam");
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void handleNetworkAgentDisconnected(Message msg) {
         NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
         disconnectAndDestroyNetwork(nai);
@@ -4211,7 +4360,7 @@
     }
 
     private void destroyNetwork(NetworkAgentInfo nai) {
-        if (nai.created) {
+        if (shouldDestroyNativeNetwork(nai)) {
             // Tell netd to clean up the configuration for this network
             // (routing rules, DNS, etc).
             // This may be slow as it requires a lot of netd shelling out to ip and
@@ -4220,15 +4369,15 @@
             // network or service a new request from an app), so network traffic isn't interrupted
             // for an unnecessarily long time.
             destroyNativeNetwork(nai);
-            mDnsManager.removeNetwork(nai.network);
-
-            // clean up tc police filters on interface.
-            if (canNetworkBeRateLimited(nai) && mIngressRateLimit >= 0) {
-                mDeps.disableIngressRateLimit(nai.linkProperties.getInterfaceName());
-            }
+        }
+        if (!nai.created && !SdkLevel.isAtLeastT()) {
+            // Backwards compatibility: send onNetworkDestroyed even if network was never created.
+            // This can never run if the code above runs because shouldDestroyNativeNetwork is
+            // false if the network was never created.
+            // TODO: delete when S is no longer supported.
+            nai.onNetworkDestroyed();
         }
         mNetIdManager.releaseNetId(nai.network.getNetId());
-        nai.onNetworkDestroyed();
     }
 
     private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) {
@@ -4261,6 +4410,9 @@
     }
 
     private void destroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
+        if (mDscpPolicyTracker != null) {
+            mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
+        }
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -4271,6 +4423,18 @@
         } catch (RemoteException | ServiceSpecificException e) {
             loge("Exception destroying network: " + e);
         }
+        // TODO: defer calling this until the network is removed from mNetworkAgentInfos.
+        // Otherwise, a private DNS configuration update for a destroyed network, or one that never
+        // gets created, could add data to DnsManager data structures that will never get deleted.
+        mDnsManager.removeNetwork(nai.network);
+
+        // clean up tc police filters on interface.
+        if (nai.everConnected && canNetworkBeRateLimited(nai) && mIngressRateLimit >= 0) {
+            mDeps.disableIngressRateLimit(nai.linkProperties.getInterfaceName());
+        }
+
+        nai.destroyed = true;
+        nai.onNetworkDestroyed();
     }
 
     // If this method proves to be too slow then we can maintain a separate
@@ -6362,7 +6526,7 @@
         if (nc.isPrivateDnsBroken()) {
             throw new IllegalArgumentException("Can't request broken private DNS");
         }
-        if (nc.hasAccessUids()) {
+        if (nc.hasAllowedUids()) {
             throw new IllegalArgumentException("Can't request access UIDs");
         }
     }
@@ -7819,7 +7983,7 @@
         final NetworkCapabilities prevNc = nai.getAndSetNetworkCapabilities(newNc);
 
         updateVpnUids(nai, prevNc, newNc);
-        updateAccessUids(nai, prevNc, newNc);
+        updateAllowedUids(nai, prevNc, newNc);
         nai.updateScoreForNetworkAgentUpdate();
 
         if (nai.getCurrentScore() == oldScore && newNc.equalRequestableCapabilities(prevNc)) {
@@ -8049,17 +8213,17 @@
         }
     }
 
-    private void updateAccessUids(@NonNull NetworkAgentInfo nai,
+    private void updateAllowedUids(@NonNull NetworkAgentInfo nai,
             @Nullable NetworkCapabilities prevNc, @Nullable NetworkCapabilities newNc) {
         // In almost all cases both NC code for empty access UIDs. return as fast as possible.
-        final boolean prevEmpty = null == prevNc || prevNc.getAccessUidsNoCopy().isEmpty();
-        final boolean newEmpty = null == newNc || newNc.getAccessUidsNoCopy().isEmpty();
+        final boolean prevEmpty = null == prevNc || prevNc.getAllowedUidsNoCopy().isEmpty();
+        final boolean newEmpty = null == newNc || newNc.getAllowedUidsNoCopy().isEmpty();
         if (prevEmpty && newEmpty) return;
 
         final ArraySet<Integer> prevUids =
-                null == prevNc ? new ArraySet<>() : prevNc.getAccessUidsNoCopy();
+                null == prevNc ? new ArraySet<>() : prevNc.getAllowedUidsNoCopy();
         final ArraySet<Integer> newUids =
-                null == newNc ? new ArraySet<>() : newNc.getAccessUidsNoCopy();
+                null == newNc ? new ArraySet<>() : newNc.getAllowedUidsNoCopy();
 
         if (prevUids.equals(newUids)) return;
 
@@ -8523,11 +8687,19 @@
                     log("   accepting network in place of " + previousSatisfier.toShortString());
                 }
                 previousSatisfier.removeRequest(previousRequest.requestId);
-                if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)) {
+                if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)
+                        && !previousSatisfier.destroyed) {
                     // If this network switch can't be supported gracefully, the request is not
                     // lingered. This allows letting go of the network sooner to reclaim some
                     // performance on the new network, since the radio can't do both at the same
                     // time while preserving good performance.
+                    //
+                    // Also don't linger the request if the old network has been destroyed.
+                    // A destroyed network does not provide actual network connectivity, so
+                    // lingering it is not useful. In particular this ensures that a destroyed
+                    // network is outscored by its replacement,
+                    // then it is torn down immediately instead of being lingered, and any apps that
+                    // were using it immediately get onLost and can connect using the new network.
                     previousSatisfier.lingerRequest(previousRequest.requestId, now);
                 }
             } else {
@@ -8992,19 +9164,6 @@
             // A network that has just connected has zero requests and is thus a foreground network.
             networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
 
-            // If a rate limit has been configured and is applicable to this network (network
-            // provides internet connectivity), apply it.
-            // Note: in case of a system server crash, there is a very small chance that this
-            // leaves some interfaces rate limited (i.e. if the rate limit had been changed just
-            // before the crash and was never applied). One solution would be to delete all
-            // potential tc police filters every time this is called. Since this is an unlikely
-            // scenario in the first place (and worst case, the interface stays rate limited until
-            // the device is rebooted), this seems a little overkill.
-            if (canNetworkBeRateLimited(networkAgent) && mIngressRateLimit >= 0) {
-                mDeps.enableIngressRateLimit(networkAgent.linkProperties.getInterfaceName(),
-                        mIngressRateLimit);
-            }
-
             if (!createNativeNetwork(networkAgent)) return;
             if (networkAgent.propagateUnderlyingCapabilities()) {
                 // Initialize the network's capabilities to their starting values according to the
@@ -9014,7 +9173,7 @@
             }
             networkAgent.created = true;
             networkAgent.onNetworkCreated();
-            updateAccessUids(networkAgent, null, networkAgent.networkCapabilities);
+            updateAllowedUids(networkAgent, null, networkAgent.networkCapabilities);
         }
 
         if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
@@ -9028,6 +9187,17 @@
             updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties),
                     null);
 
+            // If a rate limit has been configured and is applicable to this network (network
+            // provides internet connectivity), apply it. The tc police filter cannot be attached
+            // before the clsact qdisc is added which happens as part of updateLinkProperties ->
+            // updateInterfaces -> INetd#networkAddInterface.
+            // Note: in case of a system server crash, the NetworkController constructor in netd
+            // (called when netd starts up) deletes the clsact qdisc of all interfaces.
+            if (canNetworkBeRateLimited(networkAgent) && mIngressRateLimit >= 0) {
+                mDeps.enableIngressRateLimit(networkAgent.linkProperties.getInterfaceName(),
+                        mIngressRateLimit);
+            }
+
             // Until parceled LinkProperties are sent directly to NetworkMonitor, the connect
             // command must be sent after updating LinkProperties to maximize chances of
             // NetworkMonitor seeing the correct LinkProperties when starting.
@@ -9503,6 +9673,18 @@
         return ((VpnTransportInfo) ti).getType();
     }
 
+    private void maybeUpdateWifiRoamTimestamp(NetworkAgentInfo nai, NetworkCapabilities nc) {
+        if (nai == null) return;
+        final TransportInfo prevInfo = nai.networkCapabilities.getTransportInfo();
+        final TransportInfo newInfo = nc.getTransportInfo();
+        if (!(prevInfo instanceof WifiInfo) || !(newInfo instanceof WifiInfo)) {
+            return;
+        }
+        if (!TextUtils.equals(((WifiInfo)prevInfo).getBSSID(), ((WifiInfo)newInfo).getBSSID())) {
+            nai.lastRoamTimestamp = SystemClock.elapsedRealtime();
+        }
+    }
+
     /**
      * @param connectionInfo the connection to resolve.
      * @return {@code uid} if the connection is found and the app has permission to observe it
@@ -9579,14 +9761,12 @@
 
         /**
          * Event for {@link NetworkStateTrackerHandler} to trigger ConnectivityReport callbacks
-         * after processing {@link #EVENT_NETWORK_TESTED} events.
+         * after processing {@link #CMD_SEND_CONNECTIVITY_REPORT} events.
          * obj = {@link ConnectivityReportEvent} representing ConnectivityReport info reported from
          * NetworkMonitor.
          * data = PersistableBundle of extras passed from NetworkMonitor.
-         *
-         * <p>See {@link ConnectivityService#EVENT_NETWORK_TESTED}.
          */
-        private static final int EVENT_NETWORK_TESTED = ConnectivityService.EVENT_NETWORK_TESTED;
+        private static final int CMD_SEND_CONNECTIVITY_REPORT = 3;
 
         /**
          * Event for NetworkMonitor to inform ConnectivityService that a potential data stall has
@@ -9624,7 +9804,7 @@
                             (IConnectivityDiagnosticsCallback) msg.obj, msg.arg1);
                     break;
                 }
-                case EVENT_NETWORK_TESTED: {
+                case CMD_SEND_CONNECTIVITY_REPORT: {
                     final ConnectivityReportEvent reportEvent =
                             (ConnectivityReportEvent) msg.obj;
 
@@ -10517,15 +10697,16 @@
             @NonNull final UserHandle profile,
             @NonNull final ProfileNetworkPreference profileNetworkPreference) {
         final UidRange profileUids = UidRange.createForUser(profile);
-        Set<UidRange> uidRangeSet = UidRangeUtils.convertListToUidRange(
-                profileNetworkPreference.getIncludedUids());
+        Set<UidRange> uidRangeSet = UidRangeUtils.convertArrayToUidRange(
+                        profileNetworkPreference.getIncludedUids());
+
         if (uidRangeSet.size() > 0) {
             if (!UidRangeUtils.isRangeSetInUidRange(profileUids, uidRangeSet)) {
                 throw new IllegalArgumentException(
                         "Allow uid range is outside the uid range of profile.");
             }
         } else {
-            ArraySet<UidRange> disallowUidRangeSet = UidRangeUtils.convertListToUidRange(
+            ArraySet<UidRange> disallowUidRangeSet = UidRangeUtils.convertArrayToUidRange(
                     profileNetworkPreference.getExcludedUids());
             if (disallowUidRangeSet.size() > 0) {
                 if (!UidRangeUtils.isRangeSetInUidRange(profileUids, disallowUidRangeSet)) {
@@ -10684,15 +10865,22 @@
     }
 
     private boolean canNetworkBeRateLimited(@NonNull final NetworkAgentInfo networkAgent) {
-        if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) {
-            // rate limits only apply to networks that provide internet connectivity.
+        // Rate-limiting cannot run correctly before T because the BPF program is not loaded.
+        if (!SdkLevel.isAtLeastT()) return false;
+
+        final NetworkCapabilities agentCaps = networkAgent.networkCapabilities;
+        // Only test networks (they cannot hold NET_CAPABILITY_INTERNET) and networks that provide
+        // internet connectivity can be rate limited.
+        if (!agentCaps.hasCapability(NET_CAPABILITY_INTERNET) && !agentCaps.hasTransport(
+                TRANSPORT_TEST)) {
             return false;
         }
 
         final String iface = networkAgent.linkProperties.getInterfaceName();
         if (iface == null) {
-            // This can never happen.
-            logwtf("canNetworkBeRateLimited: LinkProperties#getInterfaceName returns null");
+            // This may happen in tests, but if there is no interface then there is nothing that
+            // can be rate limited.
+            loge("canNetworkBeRateLimited: LinkProperties#getInterfaceName returns null");
             return false;
         }
         return true;
@@ -11037,7 +11225,7 @@
             } else {
                 mBpfNetMaps.removeNiceApp(uid);
             }
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (ServiceSpecificException e) {
             throw new IllegalStateException(e);
         }
     }
@@ -11052,30 +11240,56 @@
             } else {
                 mBpfNetMaps.removeNaughtyApp(uid);
             }
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (ServiceSpecificException e) {
             throw new IllegalStateException(e);
         }
     }
 
     @Override
-    public void updateFirewallRule(final int chain, final int uid, final boolean allow) {
+    public void setUidFirewallRule(final int chain, final int uid, final int rule) {
         enforceNetworkStackOrSettingsPermission();
 
+        // There are only two type of firewall rule: FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+        int firewallRule = getFirewallRuleType(chain, rule);
+
+        if (firewallRule != FIREWALL_RULE_ALLOW && firewallRule != FIREWALL_RULE_DENY) {
+            throw new IllegalArgumentException("setUidFirewallRule with invalid rule: " + rule);
+        }
+
         try {
-            mBpfNetMaps.setUidRule(chain, uid,
-                    allow ? INetd.FIREWALL_RULE_ALLOW : INetd.FIREWALL_RULE_DENY);
-        } catch (RemoteException | ServiceSpecificException e) {
+            mBpfNetMaps.setUidRule(chain, uid, firewallRule);
+        } catch (ServiceSpecificException e) {
             throw new IllegalStateException(e);
         }
     }
 
+    private int getFirewallRuleType(int chain, int rule) {
+        final int defaultRule;
+        switch (chain) {
+            case ConnectivityManager.FIREWALL_CHAIN_STANDBY:
+                defaultRule = FIREWALL_RULE_ALLOW;
+                break;
+            case ConnectivityManager.FIREWALL_CHAIN_DOZABLE:
+            case ConnectivityManager.FIREWALL_CHAIN_POWERSAVE:
+            case ConnectivityManager.FIREWALL_CHAIN_RESTRICTED:
+            case ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY:
+                defaultRule = FIREWALL_RULE_DENY;
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported firewall chain: " + chain);
+        }
+        if (rule == FIREWALL_RULE_DEFAULT) rule = defaultRule;
+
+        return rule;
+    }
+
     @Override
     public void setFirewallChainEnabled(final int chain, final boolean enable) {
         enforceNetworkStackOrSettingsPermission();
 
         try {
             mBpfNetMaps.setChildChain(chain, enable);
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (ServiceSpecificException e) {
             throw new IllegalStateException(e);
         }
     }
@@ -11106,17 +11320,7 @@
                     throw new IllegalArgumentException("replaceFirewallChain with invalid chain: "
                             + chain);
             }
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void swapActiveStatsMap() {
-        enforceNetworkStackOrSettingsPermission();
-        try {
-            mBpfNetMaps.swapActiveStatsMap();
-        } catch (RemoteException | ServiceSpecificException e) {
+        } catch (ServiceSpecificException e) {
             throw new IllegalStateException(e);
         }
     }
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index fffd2be..ccc2776 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -99,12 +99,14 @@
     }
 
     /**
-     * Create a TUN or TAP interface with the given interface name and link addresses
+     * Create a TUN or TAP interface with the specified parameters.
      *
      * <p>This method will return the FileDescriptor to the interface. Close it to tear down the
      * interface.
      */
-    private TestNetworkInterface createInterface(boolean isTun, LinkAddress[] linkAddrs) {
+    @Override
+    public TestNetworkInterface createInterface(boolean isTun, boolean bringUp,
+            LinkAddress[] linkAddrs) {
         enforceTestNetworkPermissions(mContext);
 
         Objects.requireNonNull(linkAddrs, "missing linkAddrs");
@@ -122,7 +124,9 @@
                         addr.getPrefixLength());
             }
 
-            NetdUtils.setInterfaceUp(mNetd, iface);
+            if (bringUp) {
+                NetdUtils.setInterfaceUp(mNetd, iface);
+            }
 
             return new TestNetworkInterface(tunIntf, iface);
         } catch (RemoteException e) {
@@ -132,28 +136,6 @@
         }
     }
 
-    /**
-     * Create a TUN interface with the given interface name and link addresses
-     *
-     * <p>This method will return the FileDescriptor to the TUN interface. Close it to tear down the
-     * TUN interface.
-     */
-    @Override
-    public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) {
-        return createInterface(true, linkAddrs);
-    }
-
-    /**
-     * Create a TAP interface with the given interface name
-     *
-     * <p>This method will return the FileDescriptor to the TAP interface. Close it to tear down the
-     * TAP interface.
-     */
-    @Override
-    public TestNetworkInterface createTapInterface() {
-        return createInterface(false, new LinkAddress[0]);
-    }
-
     // Tracker for TestNetworkAgents
     @GuardedBy("mTestNetworkTracker")
     @NonNull
@@ -249,6 +231,7 @@
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
         nc.setNetworkSpecifier(new TestNetworkSpecifier(iface));
         nc.setAdministratorUids(administratorUids);
         if (!isMetered) {
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index b761762..b06c8aa 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -19,8 +19,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
-import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED;
-
 import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -31,7 +29,6 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkSpecifier;
 import android.net.TelephonyNetworkSpecifier;
-import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
@@ -96,11 +93,7 @@
             @NonNull final TelephonyManager t) {
         mContext = c;
         mTelephonyManager = t;
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
-            mTelephonyManagerShim = new TelephonyManagerShimImpl(mTelephonyManager);
-        } else {
-            mTelephonyManagerShim = null;
-        }
+        mTelephonyManagerShim = TelephonyManagerShimImpl.newInstance(mTelephonyManager);
         mThread = new HandlerThread(TAG);
         mThread.start();
         mHandler = new Handler(mThread.getLooper()) {};
@@ -160,7 +153,7 @@
     private void registerForCarrierChanges() {
         final IntentFilter filter = new IntentFilter();
         filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
-        mContext.registerReceiver(this, filter, null, mHandler, RECEIVER_NOT_EXPORTED /* flags */);
+        mContext.registerReceiver(this, filter, null, mHandler);
         registerCarrierPrivilegesListeners();
     }
 
@@ -194,36 +187,30 @@
 
     private void addCarrierPrivilegesListener(int logicalSlotIndex, Executor executor,
             CarrierPrivilegesListenerShim listener) {
-        if (mTelephonyManagerShim  == null) {
-            return;
-        }
         try {
             mTelephonyManagerShim.addCarrierPrivilegesListener(
                     logicalSlotIndex, executor, listener);
         } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
             Log.e(TAG, "addCarrierPrivilegesListener API is not available");
         }
     }
 
     private void removeCarrierPrivilegesListener(CarrierPrivilegesListenerShim listener) {
-        if (mTelephonyManagerShim  == null) {
-            return;
-        }
         try {
             mTelephonyManagerShim.removeCarrierPrivilegesListener(listener);
         } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
             Log.e(TAG, "removeCarrierPrivilegesListener API is not available");
         }
     }
 
     private String getCarrierServicePackageNameForLogicalSlot(int logicalSlotIndex) {
-        if (mTelephonyManagerShim  == null) {
-            return null;
-        }
         try {
             return mTelephonyManagerShim.getCarrierServicePackageNameForLogicalSlot(
                     logicalSlotIndex);
         } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
             Log.e(TAG, "getCarrierServicePackageNameForLogicalSlot API is not available");
         }
         return null;
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index c57983b..8aa5990 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -18,26 +18,41 @@
 
 import static android.net.INetd.IF_STATE_UP;
 import static android.net.INetd.PERMISSION_SYSTEM;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.INetd;
+import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpPrefix;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.TcUtils;
+import com.android.net.module.util.bpf.ClatEgress4Key;
+import com.android.net.module.util.bpf.ClatEgress4Value;
+import com.android.net.module.util.bpf.ClatIngress6Key;
+import com.android.net.module.util.bpf.ClatIngress6Value;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.util.Objects;
 
 /**
  * This coordinator is responsible for providing clat relevant functionality.
@@ -66,21 +81,46 @@
     private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");
 
     private static final int INVALID_IFINDEX = 0;
-    private static final int INVALID_PID = 0;
+
+    // For better code clarity when used for 'bool ingress' parameter.
+    @VisibleForTesting
+    static final boolean EGRESS = false;
+    @VisibleForTesting
+    static final boolean INGRESS = true;
+
+    // For better code clarity when used for 'bool ether' parameter.
+    static final boolean RAWIP = false;
+    static final boolean ETHER = true;
+
+    // The priority of clat hook - must be after tethering.
+    @VisibleForTesting
+    static final int PRIO_CLAT = 4;
+
+    private static final String CLAT_EGRESS4_MAP_PATH = makeMapPath("egress4");
+    private static final String CLAT_INGRESS6_MAP_PATH = makeMapPath("ingress6");
+
+    private static String makeMapPath(String which) {
+        return "/sys/fs/bpf/map_clatd_clat_" + which + "_map";
+    }
+
+    private static String makeProgPath(boolean ingress, boolean ether) {
+        String path = "/sys/fs/bpf/prog_clatd_schedcls_"
+                + (ingress ? "ingress6" : "egress4")
+                + "_clat_"
+                + (ether ? "ether" : "rawip");
+        return path;
+    }
 
     @NonNull
     private final INetd mNetd;
     @NonNull
     private final Dependencies mDeps;
     @Nullable
-    private String mIface = null;
+    private final IBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap;
     @Nullable
-    private String mNat64Prefix = null;
+    private final IBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap;
     @Nullable
-    private String mXlatLocalAddress4 = null;
-    @Nullable
-    private String mXlatLocalAddress6 = null;
-    private int mPid = INVALID_PID;
+    private ClatdTracker mClatdTracker = null;
 
     @VisibleForTesting
     abstract static class Dependencies {
@@ -185,9 +225,126 @@
                 throws IOException {
             native_stopClatd(iface, pfx96, v4, v6, pid);
         }
+
+        /**
+         * Tag socket as clat.
+         */
+        public long tagSocketAsClat(@NonNull FileDescriptor sock) throws IOException {
+            return native_tagSocketAsClat(sock);
+        }
+
+        /**
+         * Untag socket.
+         */
+        public void untagSocket(long cookie) throws IOException {
+            native_untagSocket(cookie);
+        }
+
+        /** Get ingress6 BPF map. */
+        @Nullable
+        public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
+            // Pre-T devices don't use ClatCoordinator to access clat map. Since Nat464Xlat
+            // initializes a ClatCoordinator object to avoid redundant null pointer check
+            // while using, ignore the BPF map initialization on pre-T devices.
+            // TODO: probably don't initialize ClatCoordinator object on pre-T devices.
+            if (!SdkLevel.isAtLeastT()) return null;
+            try {
+                return new BpfMap<>(CLAT_INGRESS6_MAP_PATH,
+                    BpfMap.BPF_F_RDWR, ClatIngress6Key.class, ClatIngress6Value.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create ingress6 map: " + e);
+                return null;
+            }
+        }
+
+        /** Get egress4 BPF map. */
+        @Nullable
+        public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
+            // Pre-T devices don't use ClatCoordinator to access clat map. Since Nat464Xlat
+            // initializes a ClatCoordinator object to avoid redundant null pointer check
+            // while using, ignore the BPF map initialization on pre-T devices.
+            // TODO: probably don't initialize ClatCoordinator object on pre-T devices.
+            if (!SdkLevel.isAtLeastT()) return null;
+            try {
+                return new BpfMap<>(CLAT_EGRESS4_MAP_PATH,
+                    BpfMap.BPF_F_RDWR, ClatEgress4Key.class, ClatEgress4Value.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create egress4 map: " + e);
+                return null;
+            }
+        }
+
+        /** Checks if the network interface uses an ethernet L2 header. */
+        public boolean isEthernet(String iface) throws IOException {
+            return TcUtils.isEthernet(iface);
+        }
+
+        /** Add a clsact qdisc. */
+        public void tcQdiscAddDevClsact(int ifIndex) throws IOException {
+            TcUtils.tcQdiscAddDevClsact(ifIndex);
+        }
+
+        /** Attach a tc bpf filter. */
+        public void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, short proto,
+                String bpfProgPath) throws IOException {
+            TcUtils.tcFilterAddDevBpf(ifIndex, ingress, prio, proto, bpfProgPath);
+        }
+
+        /** Delete a tc filter. */
+        public void tcFilterDelDev(int ifIndex, boolean ingress, short prio, short proto)
+                throws IOException {
+            TcUtils.tcFilterDelDev(ifIndex, ingress, prio, proto);
+        }
     }
 
     @VisibleForTesting
+    static class ClatdTracker {
+        @NonNull
+        public final String iface;
+        public final int ifIndex;
+        @NonNull
+        public final String v4iface;
+        public final int v4ifIndex;
+        @NonNull
+        public final Inet4Address v4;
+        @NonNull
+        public final Inet6Address v6;
+        @NonNull
+        public final Inet6Address pfx96;
+        public final int pid;
+        public final long cookie;
+
+        ClatdTracker(@NonNull String iface, int ifIndex, @NonNull String v4iface,
+                int v4ifIndex, @NonNull Inet4Address v4, @NonNull Inet6Address v6,
+                @NonNull Inet6Address pfx96, int pid, long cookie) {
+            this.iface = iface;
+            this.ifIndex = ifIndex;
+            this.v4iface = v4iface;
+            this.v4ifIndex = v4ifIndex;
+            this.v4 = v4;
+            this.v6 = v6;
+            this.pfx96 = pfx96;
+            this.pid = pid;
+            this.cookie = cookie;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof ClatdTracker)) return false;
+            ClatdTracker that = (ClatdTracker) o;
+            return Objects.equals(this.iface, that.iface)
+                    && this.ifIndex == that.ifIndex
+                    && Objects.equals(this.v4iface, that.v4iface)
+                    && this.v4ifIndex == that.v4ifIndex
+                    && Objects.equals(this.v4, that.v4)
+                    && Objects.equals(this.v6, that.v6)
+                    && Objects.equals(this.pfx96, that.pfx96)
+                    && this.pid == that.pid
+                    && this.cookie == that.cookie;
+        }
+    };
+
+    @VisibleForTesting
     static int getFwmark(int netId) {
         // See union Fwmark in system/netd/include/Fwmark.h
         return (netId & 0xffff)
@@ -211,6 +368,129 @@
     public ClatCoordinator(@NonNull Dependencies deps) {
         mDeps = deps;
         mNetd = mDeps.getNetd();
+        mIngressMap = mDeps.getBpfIngress6Map();
+        mEgressMap = mDeps.getBpfEgress4Map();
+    }
+
+    private void maybeStartBpf(final ClatdTracker tracker) {
+        if (mIngressMap == null || mEgressMap == null) return;
+
+        final boolean isEthernet;
+        try {
+            isEthernet = mDeps.isEthernet(tracker.iface);
+        } catch (IOException e) {
+            Log.e(TAG, "Fail to call isEthernet for interface " + tracker.iface);
+            return;
+        }
+
+        final ClatEgress4Key txKey = new ClatEgress4Key(tracker.v4ifIndex, tracker.v4);
+        final ClatEgress4Value txValue = new ClatEgress4Value(tracker.ifIndex, tracker.v6,
+                tracker.pfx96, (short) (isEthernet ? 1 /* ETHER */ : 0 /* RAWIP */));
+        try {
+            mEgressMap.insertEntry(txKey, txValue);
+        } catch (ErrnoException | IllegalStateException e) {
+            Log.e(TAG, "Could not insert entry (" + txKey + ", " + txValue + ") on egress map: "
+                    + e);
+            return;
+        }
+
+        final ClatIngress6Key rxKey = new ClatIngress6Key(tracker.ifIndex, tracker.pfx96,
+                tracker.v6);
+        final ClatIngress6Value rxValue = new ClatIngress6Value(tracker.v4ifIndex,
+                tracker.v4);
+        try {
+            mIngressMap.insertEntry(rxKey, rxValue);
+        } catch (ErrnoException | IllegalStateException e) {
+            Log.e(TAG, "Could not insert entry (" + rxKey + ", " + rxValue + ") ingress map: "
+                    + e);
+            try {
+                mEgressMap.deleteEntry(txKey);
+            } catch (ErrnoException | IllegalStateException e2) {
+                Log.e(TAG, "Could not delete entry (" + txKey + ") from egress map: " + e2);
+            }
+            return;
+        }
+
+        // Usually the clsact will be added in netd RouteController::addInterfaceToPhysicalNetwork.
+        // But clat is started before the v4- interface is added to the network. The clat startup
+        // have to add clsact of v4- tun interface first for adding bpf filter in maybeStartBpf.
+        try {
+            // tc qdisc add dev .. clsact
+            mDeps.tcQdiscAddDevClsact(tracker.v4ifIndex);
+        } catch (IOException e) {
+            Log.e(TAG, "tc qdisc add dev (" + tracker.v4ifIndex + "[" + tracker.v4iface
+                    + "]) failure: " + e);
+            try {
+                mEgressMap.deleteEntry(txKey);
+            } catch (ErrnoException | IllegalStateException e2) {
+                Log.e(TAG, "Could not delete entry (" + txKey + ") from egress map: " + e2);
+            }
+            try {
+                mIngressMap.deleteEntry(rxKey);
+            } catch (ErrnoException | IllegalStateException e3) {
+                Log.e(TAG, "Could not delete entry (" + rxKey + ") from ingress map: " + e3);
+            }
+            return;
+        }
+
+        // This program will be attached to the v4-* interface which is a TUN and thus always rawip.
+        try {
+            // tc filter add dev .. egress prio 4 protocol ip bpf object-pinned /sys/fs/bpf/...
+            // direct-action
+            mDeps.tcFilterAddDevBpf(tracker.v4ifIndex, EGRESS, (short) PRIO_CLAT, (short) ETH_P_IP,
+                    makeProgPath(EGRESS, RAWIP));
+        } catch (IOException e) {
+            Log.e(TAG, "tc filter add dev (" + tracker.v4ifIndex + "[" + tracker.v4iface
+                    + "]) egress prio PRIO_CLAT protocol ip failure: " + e);
+
+            // The v4- interface clsact is not deleted for unwinding error because once it is
+            // created with interface addition, the lifetime is till interface deletion. Moreover,
+            // the clsact has no clat filter now. It should not break anything.
+
+            try {
+                mEgressMap.deleteEntry(txKey);
+            } catch (ErrnoException | IllegalStateException e2) {
+                Log.e(TAG, "Could not delete entry (" + txKey + ") from egress map: " + e2);
+            }
+            try {
+                mIngressMap.deleteEntry(rxKey);
+            } catch (ErrnoException | IllegalStateException e3) {
+                Log.e(TAG, "Could not delete entry (" + rxKey + ") from ingress map: " + e3);
+            }
+            return;
+        }
+
+        try {
+            // tc filter add dev .. ingress prio 4 protocol ipv6 bpf object-pinned /sys/fs/bpf/...
+            // direct-action
+            mDeps.tcFilterAddDevBpf(tracker.ifIndex, INGRESS, (short) PRIO_CLAT,
+                    (short) ETH_P_IPV6, makeProgPath(INGRESS, isEthernet));
+        } catch (IOException e) {
+            Log.e(TAG, "tc filter add dev (" + tracker.ifIndex + "[" + tracker.iface
+                    + "]) ingress prio PRIO_CLAT protocol ipv6 failure: " + e);
+
+            // The v4- interface clsact is not deleted. See the reason in the error unwinding code
+            // of the egress filter attaching of v4- tun interface.
+
+            try {
+                mDeps.tcFilterDelDev(tracker.v4ifIndex, EGRESS, (short) PRIO_CLAT,
+                        (short) ETH_P_IP);
+            } catch (IOException e2) {
+                Log.e(TAG, "tc filter del dev (" + tracker.v4ifIndex + "[" + tracker.v4iface
+                        + "]) egress prio PRIO_CLAT protocol ip failure: " + e2);
+            }
+            try {
+                mEgressMap.deleteEntry(txKey);
+            } catch (ErrnoException | IllegalStateException e3) {
+                Log.e(TAG, "Could not delete entry (" + txKey + ") from egress map: " + e3);
+            }
+            try {
+                mIngressMap.deleteEntry(rxKey);
+            } catch (ErrnoException | IllegalStateException e4) {
+                Log.e(TAG, "Could not delete entry (" + rxKey + ") from ingress map: " + e4);
+            }
+            return;
+        }
     }
 
     /**
@@ -219,30 +499,46 @@
     public String clatStart(final String iface, final int netId,
             @NonNull final IpPrefix nat64Prefix)
             throws IOException {
-        if (mIface != null || mPid != INVALID_PID) {
-            throw new IOException("Clatd is already running on " + mIface + " (pid " + mPid + ")");
+        if (mClatdTracker != null) {
+            throw new IOException("Clatd is already running on " + mClatdTracker.iface
+                    + " (pid " + mClatdTracker.pid + ")");
         }
         if (nat64Prefix.getPrefixLength() != 96) {
             throw new IOException("Prefix must be 96 bits long: " + nat64Prefix);
         }
 
         // [1] Pick an IPv4 address from 192.0.0.4, 192.0.0.5, 192.0.0.6 ..
-        final String v4;
+        final String v4Str;
         try {
-            v4 = mDeps.selectIpv4Address(INIT_V4ADDR_STRING, INIT_V4ADDR_PREFIX_LEN);
+            v4Str = mDeps.selectIpv4Address(INIT_V4ADDR_STRING, INIT_V4ADDR_PREFIX_LEN);
         } catch (IOException e) {
             throw new IOException("no IPv4 addresses were available for clat: " + e);
         }
 
-        // [2] Generate a checksum-neutral IID.
-        final String pfx96 = nat64Prefix.getAddress().getHostAddress();
-        final String v6;
+        final Inet4Address v4;
         try {
-            v6 = mDeps.generateIpv6Address(iface, v4, pfx96);
+            v4 = (Inet4Address) InetAddresses.parseNumericAddress(v4Str);
+        } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
+            throw new IOException("Invalid IPv4 address " + v4Str);
+        }
+
+        // [2] Generate a checksum-neutral IID.
+        final String pfx96Str = nat64Prefix.getAddress().getHostAddress();
+        final String v6Str;
+        try {
+            v6Str = mDeps.generateIpv6Address(iface, v4Str, pfx96Str);
         } catch (IOException e) {
             throw new IOException("no IPv6 addresses were available for clat: " + e);
         }
 
+        final Inet6Address pfx96 = (Inet6Address) nat64Prefix.getAddress();
+        final Inet6Address v6;
+        try {
+            v6 = (Inet6Address) InetAddresses.parseNumericAddress(v6Str);
+        } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
+            throw new IOException("Invalid IPv6 address " + v6Str);
+        }
+
         // [3] Open, configure and bring up the tun interface.
         // Create the v4-... tun interface.
         final String tunIface = CLAT_PREFIX + iface;
@@ -253,6 +549,12 @@
             throw new IOException("Create tun interface " + tunIface + " failed: " + e);
         }
 
+        final int tunIfIndex = mDeps.getInterfaceIndex(tunIface);
+        if (tunIfIndex == INVALID_IFINDEX) {
+            tunFd.close();
+            throw new IOException("Fail to get interface index for interface " + tunIface);
+        }
+
         // disable IPv6 on it - failing to do so is not a critical error
         try {
             mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
@@ -263,7 +565,7 @@
 
         // Detect ipv4 mtu.
         final Integer fwmark = getFwmark(netId);
-        final int detectedMtu = mDeps.detectMtu(pfx96,
+        final int detectedMtu = mDeps.detectMtu(pfx96Str,
                 ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
         final int mtu = adjustMtu(detectedMtu);
         Log.i(TAG, "ipv4 mtu is " + mtu);
@@ -279,7 +581,7 @@
         }
         final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
         ifConfig.ifName = tunIface;
-        ifConfig.ipv4Addr = v4;
+        ifConfig.ipv4Addr = v4Str;
         ifConfig.prefixLength = 32;
         ifConfig.hwAddr = "";
         ifConfig.flags = new String[] {IF_STATE_UP};
@@ -317,8 +619,8 @@
             throw new IOException("Open raw socket failed: " + e);
         }
 
-        final int ifaceIndex = mDeps.getInterfaceIndex(iface);
-        if (ifaceIndex == INVALID_IFINDEX) {
+        final int ifIndex = mDeps.getInterfaceIndex(iface);
+        if (ifIndex == INVALID_IFINDEX) {
             tunFd.close();
             readSock6.close();
             writeSock6.close();
@@ -327,7 +629,7 @@
 
         // Start translating packets to the new prefix.
         try {
-            mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6, ifaceIndex);
+            mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6Str, ifIndex);
         } catch (IOException e) {
             tunFd.close();
             readSock6.close();
@@ -335,9 +637,20 @@
             throw new IOException("add anycast sockopt failed: " + e);
         }
 
+        // Tag socket as AID_CLAT to avoid duplicated CLAT data usage accounting.
+        final long cookie;
+        try {
+            cookie = mDeps.tagSocketAsClat(writeSock6.getFileDescriptor());
+        } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
+            throw new IOException("tag raw socket failed: " + e);
+        }
+
         // Update our packet socket filter to reflect the new 464xlat IP address.
         try {
-            mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6, ifaceIndex);
+            mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6Str, ifIndex);
         } catch (IOException e) {
             tunFd.close();
             readSock6.close();
@@ -346,14 +659,13 @@
         }
 
         // [5] Start clatd.
+        final int pid;
         try {
-            mPid = mDeps.startClatd(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
-                    writeSock6.getFileDescriptor(), iface, pfx96, v4, v6);
-            mIface = iface;
-            mNat64Prefix = pfx96;
-            mXlatLocalAddress4 = v4;
-            mXlatLocalAddress6 = v6;
+            pid = mDeps.startClatd(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
+                    writeSock6.getFileDescriptor(), iface, pfx96Str, v4Str, v6Str);
         } catch (IOException e) {
+            // TODO: probably refactor to handle the exception of #untagSocket if any.
+            mDeps.untagSocket(cookie);
             throw new IOException("Error start clatd on " + iface + ": " + e);
         } finally {
             tunFd.close();
@@ -361,28 +673,78 @@
             writeSock6.close();
         }
 
-        return v6;
+        // [6] Initialize and store clatd tracker object.
+        mClatdTracker = new ClatdTracker(iface, ifIndex, tunIface, tunIfIndex, v4, v6, pfx96,
+                pid, cookie);
+
+        // [7] Start BPF
+        maybeStartBpf(mClatdTracker);
+
+        return v6Str;
+    }
+
+    private void maybeStopBpf(final ClatdTracker tracker) {
+        if (mIngressMap == null || mEgressMap == null) return;
+
+        try {
+            mDeps.tcFilterDelDev(tracker.ifIndex, INGRESS, (short) PRIO_CLAT, (short) ETH_P_IPV6);
+        } catch (IOException e) {
+            Log.e(TAG, "tc filter del dev (" + tracker.ifIndex + "[" + tracker.iface
+                    + "]) ingress prio PRIO_CLAT protocol ipv6 failure: " + e);
+        }
+
+        try {
+            mDeps.tcFilterDelDev(tracker.v4ifIndex, EGRESS, (short) PRIO_CLAT, (short) ETH_P_IP);
+        } catch (IOException e) {
+            Log.e(TAG, "tc filter del dev (" + tracker.v4ifIndex + "[" + tracker.v4iface
+                    + "]) egress prio PRIO_CLAT protocol ip failure: " + e);
+        }
+
+        // We cleanup the maps last, so scanning through them can be used to
+        // determine what still needs cleanup.
+
+        final ClatEgress4Key txKey = new ClatEgress4Key(tracker.v4ifIndex, tracker.v4);
+        try {
+            mEgressMap.deleteEntry(txKey);
+        } catch (ErrnoException | IllegalStateException e) {
+            Log.e(TAG, "Could not delete entry (" + txKey + "): " + e);
+        }
+
+        final ClatIngress6Key rxKey = new ClatIngress6Key(tracker.ifIndex, tracker.pfx96,
+                tracker.v6);
+        try {
+            mIngressMap.deleteEntry(rxKey);
+        } catch (ErrnoException | IllegalStateException e) {
+            Log.e(TAG, "Could not delete entry (" + rxKey + "): " + e);
+        }
     }
 
     /**
      * Stop clatd
      */
     public void clatStop() throws IOException {
-        if (mPid == INVALID_PID) {
+        if (mClatdTracker == null) {
             throw new IOException("Clatd has not started");
         }
-        Log.i(TAG, "Stopping clatd pid=" + mPid + " on " + mIface);
+        Log.i(TAG, "Stopping clatd pid=" + mClatdTracker.pid + " on " + mClatdTracker.iface);
 
-        mDeps.stopClatd(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6, mPid);
-        // TODO: remove setIptablesDropRule
+        maybeStopBpf(mClatdTracker);
+        mDeps.stopClatd(mClatdTracker.iface, mClatdTracker.pfx96.getHostAddress(),
+                mClatdTracker.v4.getHostAddress(), mClatdTracker.v6.getHostAddress(),
+                mClatdTracker.pid);
+        mDeps.untagSocket(mClatdTracker.cookie);
 
-        Log.i(TAG, "clatd on " + mIface + " stopped");
+        Log.i(TAG, "clatd on " + mClatdTracker.iface + " stopped");
+        mClatdTracker = null;
+    }
 
-        mIface = null;
-        mNat64Prefix = null;
-        mXlatLocalAddress4 = null;
-        mXlatLocalAddress6 = null;
-        mPid = INVALID_PID;
+    /**
+     * Get clatd tracker. For test only.
+     */
+    @VisibleForTesting
+    @Nullable
+    ClatdTracker getClatdTrackerForTesting() {
+        return mClatdTracker;
     }
 
     private static native String native_selectIpv4Address(String v4addr, int prefixlen)
@@ -403,4 +765,6 @@
             throws IOException;
     private static native void native_stopClatd(String iface, String pfx96, String v4, String v6,
             int pid) throws IOException;
+    private static native long native_tagSocketAsClat(FileDescriptor sock) throws IOException;
+    private static native void native_untagSocket(long cookie) throws IOException;
 }
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
new file mode 100644
index 0000000..cde6ea7
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.connectivity.aidl.ConnectivityNative;
+import android.os.Binder;
+import android.os.Process;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BpfBitmap;
+import com.android.net.module.util.BpfUtils;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * @hide
+ */
+public class ConnectivityNativeService extends ConnectivityNative.Stub {
+    public static final String SERVICE_NAME = "connectivity_native";
+
+    private static final String TAG = ConnectivityNativeService.class.getSimpleName();
+    private static final String CGROUP_PATH = "/sys/fs/cgroup";
+    private static final String V4_PROG_PATH =
+            "/sys/fs/bpf/prog_block_bind4_block_port";
+    private static final String V6_PROG_PATH =
+            "/sys/fs/bpf/prog_block_bind6_block_port";
+    private static final String BLOCKED_PORTS_MAP_PATH = "/sys/fs/bpf/map_block_blocked_ports_map";
+
+    private final Context mContext;
+
+    // BPF map for port blocking. Exactly 65536 entries long, with one entry per port number
+    @Nullable
+    private final BpfBitmap mBpfBlockedPortsMap;
+
+    /**
+     * Dependencies of ConnectivityNativeService, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Get BPF maps. */
+        @Nullable public BpfBitmap getBlockPortsMap() {
+            try {
+                return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);
+            } catch (ErrnoException e) {
+                throw new UnsupportedOperationException("Failed to create blocked ports map: "
+                        + e);
+            }
+        }
+    }
+
+    private void enforceBlockPortPermission() {
+        final int uid = Binder.getCallingUid();
+        if (uid == Process.ROOT_UID || uid == Process.PHONE_UID) return;
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+    }
+
+    private void ensureValidPortNumber(int port) {
+        if (port < 0 || port > 65535) {
+            throw new IllegalArgumentException("Invalid port number " + port);
+        }
+    }
+
+    public ConnectivityNativeService(final Context context) {
+        this(context, new Dependencies());
+    }
+
+    @VisibleForTesting
+    protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) {
+        mContext = context;
+        mBpfBlockedPortsMap = deps.getBlockPortsMap();
+        attachProgram();
+    }
+
+    @Override
+    public void blockPortForBind(int port) {
+        enforceBlockPortPermission();
+        ensureValidPortNumber(port);
+        try {
+            mBpfBlockedPortsMap.set(port);
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno, e.getMessage());
+        }
+    }
+
+    @Override
+    public void unblockPortForBind(int port) {
+        enforceBlockPortPermission();
+        ensureValidPortNumber(port);
+        try {
+            mBpfBlockedPortsMap.unset(port);
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Could not unset bitmap value for (port: " + port + "): " + e);
+        }
+    }
+
+    @Override
+    public void unblockAllPortsForBind() {
+        enforceBlockPortPermission();
+        try {
+            mBpfBlockedPortsMap.clear();
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno, "Could not clear map: " + e);
+        }
+    }
+
+    @Override
+    public int[] getPortsBlockedForBind() {
+        enforceBlockPortPermission();
+
+        ArrayList<Integer> portMap = new ArrayList<Integer>();
+        for (int i = 0; i <= 65535; i++) {
+            try {
+                if (mBpfBlockedPortsMap.get(i)) portMap.add(i);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Failed to get index " + i, e);
+            }
+        }
+        return CollectionUtils.toIntArray(portMap);
+    }
+
+    @Override
+    public int getInterfaceVersion() {
+        return this.VERSION;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        return this.HASH;
+    }
+
+    /**
+     * Attach BPF program
+     */
+    private void attachProgram() {
+        try {
+            BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0);
+        } catch (IOException e) {
+            throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: "
+                    + e);
+        }
+        try {
+            BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0);
+        } catch (IOException e) {
+            throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: "
+                    + e);
+        }
+        Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs");
+    }
+}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 43cfc8f..de9dfe3 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -16,10 +16,11 @@
 
 package com.android.server.connectivity;
 
-import static android.net.DscpPolicy.STATUS_DELETED;
-import static android.net.DscpPolicy.STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
-import static android.net.DscpPolicy.STATUS_POLICY_NOT_FOUND;
-import static android.net.DscpPolicy.STATUS_SUCCESS;
+import static android.net.NetworkAgent.DSCP_POLICY_STATUS_DELETED;
+import static android.net.NetworkAgent.DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+import static android.net.NetworkAgent.DSCP_POLICY_STATUS_POLICY_NOT_FOUND;
+import static android.net.NetworkAgent.DSCP_POLICY_STATUS_REQUEST_DECLINED;
+import static android.net.NetworkAgent.DSCP_POLICY_STATUS_SUCCESS;
 import static android.system.OsConstants.ETH_P_ALL;
 
 import android.annotation.NonNull;
@@ -37,6 +38,7 @@
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.NetworkInterface;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -66,30 +68,68 @@
 
     private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv4Policies;
     private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv6Policies;
-    private final SparseIntArray mPolicyIdToBpfMapIndex;
+
+    // The actual policy rules used by the BPF code to process packets
+    // are in mBpfDscpIpv4Policies and mBpfDscpIpv4Policies. Both of
+    // these can contain up to MAX_POLICIES rules.
+    //
+    // A given policy always consumes one entry in both the IPv4 and
+    // IPv6 maps even if if's an IPv4-only or IPv6-only policy.
+    //
+    // Each interface index has a SparseIntArray of rules which maps a
+    // policy ID to the index of the corresponding rule in the maps.
+    // mIfaceIndexToPolicyIdBpfMapIndex maps the interface index to
+    // the per-interface SparseIntArray.
+    private final HashMap<Integer, SparseIntArray> mIfaceIndexToPolicyIdBpfMapIndex;
 
     public DscpPolicyTracker() throws ErrnoException {
         mAttachedIfaces = new HashSet<String>();
-
-        mPolicyIdToBpfMapIndex = new SparseIntArray(MAX_POLICIES);
+        mIfaceIndexToPolicyIdBpfMapIndex = new HashMap<Integer, SparseIntArray>();
         mBpfDscpIpv4Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
                 BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
         mBpfDscpIpv6Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
                 BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
     }
 
+    private boolean isUnusedIndex(int index) {
+        for (SparseIntArray ifacePolicies : mIfaceIndexToPolicyIdBpfMapIndex.values()) {
+            if (ifacePolicies.indexOfValue(index) >= 0) return false;
+        }
+        return true;
+    }
+
     private int getFirstFreeIndex() {
+        if (mIfaceIndexToPolicyIdBpfMapIndex.size() == 0) return 0;
         for (int i = 0; i < MAX_POLICIES; i++) {
-            if (mPolicyIdToBpfMapIndex.indexOfValue(i) < 0) return i;
+            if (isUnusedIndex(i)) {
+                return i;
+            }
         }
         return MAX_POLICIES;
     }
 
+    private int findIndex(int policyId, int ifIndex) {
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(ifIndex);
+        if (ifacePolicies != null) {
+            final int existingIndex = ifacePolicies.get(policyId, -1);
+            if (existingIndex != -1) {
+                return existingIndex;
+            }
+        }
+
+        final int firstIndex = getFirstFreeIndex();
+        if (firstIndex >= MAX_POLICIES) {
+            // New policy is being added, but max policies has already been reached.
+            return -1;
+        }
+        return firstIndex;
+    }
+
     private void sendStatus(NetworkAgentInfo nai, int policyId, int status) {
         try {
             nai.networkAgent.onDscpPolicyStatusUpdated(policyId, status);
         } catch (RemoteException e) {
-            Log.d(TAG, "Failed update policy status: ", e);
+            Log.e(TAG, "Failed update policy status: ", e);
         }
     }
 
@@ -107,37 +147,43 @@
                         || policy.getSourceAddress() instanceof Inet6Address));
     }
 
-    private int addDscpPolicyInternal(DscpPolicy policy) {
+    private int getIfaceIndex(NetworkAgentInfo nai) {
+        String iface = nai.linkProperties.getInterfaceName();
+        NetworkInterface netIface;
+        try {
+            netIface = NetworkInterface.getByName(iface);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to get iface index for " + iface + ": " + e);
+            netIface = null;
+        }
+        return (netIface != null) ? netIface.getIndex() : 0;
+    }
+
+    private int addDscpPolicyInternal(DscpPolicy policy, int ifIndex) {
         // If there is no existing policy with a matching ID, and we are already at
         // the maximum number of policies then return INSUFFICIENT_PROCESSING_RESOURCES.
-        final int existingIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId(), -1);
-        if (existingIndex == -1 && mPolicyIdToBpfMapIndex.size() >= MAX_POLICIES) {
-            return STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(ifIndex);
+        if (ifacePolicies == null) {
+            ifacePolicies = new SparseIntArray(MAX_POLICIES);
         }
 
         // Currently all classifiers are supported, if any are removed return
-        // STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
-        // and for any other generic error STATUS_REQUEST_DECLINED
+        // DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
+        // and for any other generic error DSCP_POLICY_STATUS_REQUEST_DECLINED
 
-        int addIndex = 0;
-        // If a policy with a matching ID exists, replace it, otherwise use the next free
-        // index for the policy.
-        if (existingIndex != -1) {
-            addIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId());
-        } else {
-            addIndex = getFirstFreeIndex();
+        final int addIndex = findIndex(policy.getPolicyId(), ifIndex);
+        if (addIndex == -1) {
+            return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
         }
 
         try {
-            mPolicyIdToBpfMapIndex.put(policy.getPolicyId(), addIndex);
-
             // Add v4 policy to mBpfDscpIpv4Policies if source and destination address
-            // are both null or if they are both instances of Inet6Address.
+            // are both null or if they are both instances of Inet4Address.
             if (matchesIpv4(policy)) {
                 mBpfDscpIpv4Policies.insertOrReplaceEntry(
                         new Struct.U32(addIndex),
                         new DscpPolicyValue(policy.getSourceAddress(),
-                            policy.getDestinationAddress(),
+                            policy.getDestinationAddress(), ifIndex,
                             policy.getSourcePort(), policy.getDestinationPortRange(),
                             (short) policy.getProtocol(), (short) policy.getDscpValue()));
             }
@@ -148,49 +194,67 @@
                 mBpfDscpIpv6Policies.insertOrReplaceEntry(
                         new Struct.U32(addIndex),
                         new DscpPolicyValue(policy.getSourceAddress(),
-                                policy.getDestinationAddress(),
+                                policy.getDestinationAddress(), ifIndex,
                                 policy.getSourcePort(), policy.getDestinationPortRange(),
                                 (short) policy.getProtocol(), (short) policy.getDscpValue()));
             }
+
+            ifacePolicies.put(policy.getPolicyId(), addIndex);
+            // Only add the policy to the per interface map if the policy was successfully
+            // added to both bpf maps above. It is safe to assume that if insert fails for
+            // one map then it fails for both.
+            mIfaceIndexToPolicyIdBpfMapIndex.put(ifIndex, ifacePolicies);
         } catch (ErrnoException e) {
             Log.e(TAG, "Failed to insert policy into map: ", e);
-            return STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+            return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
         }
 
-        return STATUS_SUCCESS;
+        return DSCP_POLICY_STATUS_SUCCESS;
     }
 
     /**
      * Add the provided DSCP policy to the bpf map. Attach bpf program dscp_policy to iface
      * if not already attached. Response will be sent back to nai with status.
      *
-     * STATUS_SUCCESS - if policy was added successfully
-     * STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set
+     * DSCP_POLICY_STATUS_SUCCESS - if policy was added successfully
+     * DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set
+     * DSCP_POLICY_STATUS_REQUEST_DECLINED - Interface index was invalid
      */
     public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) {
         if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
             if (!attachProgram(nai.linkProperties.getInterfaceName())) {
                 Log.e(TAG, "Unable to attach program");
-                sendStatus(nai, policy.getPolicyId(), STATUS_INSUFFICIENT_PROCESSING_RESOURCES);
+                sendStatus(nai, policy.getPolicyId(),
+                        DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES);
                 return;
             }
         }
 
-        int status = addDscpPolicyInternal(policy);
+        final int ifIndex = getIfaceIndex(nai);
+        if (ifIndex == 0) {
+            Log.e(TAG, "Iface index is invalid");
+            sendStatus(nai, policy.getPolicyId(), DSCP_POLICY_STATUS_REQUEST_DECLINED);
+            return;
+        }
+
+        int status = addDscpPolicyInternal(policy, ifIndex);
         sendStatus(nai, policy.getPolicyId(), status);
     }
 
-    private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index) {
-        int status = STATUS_POLICY_NOT_FOUND;
+    private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index,
+            boolean sendCallback) {
+        int status = DSCP_POLICY_STATUS_POLICY_NOT_FOUND;
         try {
             mBpfDscpIpv4Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
             mBpfDscpIpv6Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
-            status = STATUS_DELETED;
+            status = DSCP_POLICY_STATUS_DELETED;
         } catch (ErrnoException e) {
             Log.e(TAG, "Failed to delete policy from map: ", e);
         }
 
-        sendStatus(nai, policyId, status);
+        if (sendCallback) {
+            sendStatus(nai, policyId, status);
+        }
     }
 
     /**
@@ -199,40 +263,48 @@
     public void removeDscpPolicy(NetworkAgentInfo nai, int policyId) {
         if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
             // Nothing to remove since program is not attached. Send update back for policy id.
-            sendStatus(nai, policyId, STATUS_POLICY_NOT_FOUND);
+            sendStatus(nai, policyId, DSCP_POLICY_STATUS_POLICY_NOT_FOUND);
             return;
         }
 
-        if (mPolicyIdToBpfMapIndex.get(policyId, -1) != -1) {
-            removePolicyFromMap(nai, policyId, mPolicyIdToBpfMapIndex.get(policyId));
-            mPolicyIdToBpfMapIndex.delete(policyId);
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(getIfaceIndex(nai));
+        if (ifacePolicies == null) return;
+
+        final int existingIndex = ifacePolicies.get(policyId, -1);
+        if (existingIndex == -1) {
+            Log.e(TAG, "Policy " + policyId + " does not exist in map.");
+            sendStatus(nai, policyId, DSCP_POLICY_STATUS_POLICY_NOT_FOUND);
+            return;
         }
 
-        // TODO: detach should only occur if no more policies are present on the nai's iface.
-        if (mPolicyIdToBpfMapIndex.size() == 0) {
+        removePolicyFromMap(nai, policyId, existingIndex, true);
+        ifacePolicies.delete(policyId);
+
+        if (ifacePolicies.size() == 0) {
             detachProgram(nai.linkProperties.getInterfaceName());
         }
     }
 
     /**
-     * Remove all DSCP policies and detach program.
+     * Remove all DSCP policies and detach program. Send callback if requested.
      */
-    // TODO: Remove all should only remove policies from corresponding nai iface.
-    public void removeAllDscpPolicies(NetworkAgentInfo nai) {
+    public void removeAllDscpPolicies(NetworkAgentInfo nai, boolean sendCallback) {
         if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
             // Nothing to remove since program is not attached. Send update for policy
             // id 0. The status update must contain a policy ID, and 0 is an invalid id.
-            sendStatus(nai, 0, STATUS_SUCCESS);
+            if (sendCallback) {
+                sendStatus(nai, 0, DSCP_POLICY_STATUS_SUCCESS);
+            }
             return;
         }
 
-        for (int i = 0; i < mPolicyIdToBpfMapIndex.size(); i++) {
-            removePolicyFromMap(nai, mPolicyIdToBpfMapIndex.keyAt(i),
-                    mPolicyIdToBpfMapIndex.valueAt(i));
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(getIfaceIndex(nai));
+        if (ifacePolicies == null) return;
+        for (int i = 0; i < ifacePolicies.size(); i++) {
+            removePolicyFromMap(nai, ifacePolicies.keyAt(i), ifacePolicies.valueAt(i),
+                    sendCallback);
         }
-        mPolicyIdToBpfMapIndex.clear();
-
-        // Can detach program since no policies are active.
+        ifacePolicies.clear();
         detachProgram(nai.linkProperties.getInterfaceName());
     }
 
@@ -240,12 +312,12 @@
      * Attach BPF program
      */
     private boolean attachProgram(@NonNull String iface) {
-        // TODO: attach needs to be per iface not program.
-
         try {
             NetworkInterface netIface = NetworkInterface.getByName(iface);
+            boolean isEth = TcUtils.isEthernet(iface);
+            String path = PROG_PATH + (isEth ? "_ether" : "_raw_ip");
             TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL,
-                    PROG_PATH);
+                    path);
         } catch (IOException e) {
             Log.e(TAG, "Unable to attach to TC on " + iface + ": " + e);
             return false;
@@ -263,9 +335,9 @@
             if (netIface != null) {
                 TcUtils.tcFilterDelDev(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL);
             }
+            mAttachedIfaces.remove(iface);
         } catch (IOException e) {
             Log.e(TAG, "Unable to detach to TC on " + iface + ": " + e);
         }
-        mAttachedIfaces.remove(iface);
     }
 }
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index cb40306..6e4e7eb 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -31,29 +31,31 @@
 public class DscpPolicyValue extends Struct {
     private static final String TAG = DscpPolicyValue.class.getSimpleName();
 
-    // TODO: add the interface index.
     @Field(order = 0, type = Type.ByteArray, arraysize = 16)
     public final byte[] src46;
 
     @Field(order = 1, type = Type.ByteArray, arraysize = 16)
     public final byte[] dst46;
 
-    @Field(order = 2, type = Type.UBE16)
-    public final int srcPort;
+    @Field(order = 2, type = Type.U32)
+    public final long ifIndex;
 
     @Field(order = 3, type = Type.UBE16)
-    public final int dstPortStart;
+    public final int srcPort;
 
     @Field(order = 4, type = Type.UBE16)
+    public final int dstPortStart;
+
+    @Field(order = 5, type = Type.UBE16)
     public final int dstPortEnd;
 
-    @Field(order = 5, type = Type.U8)
+    @Field(order = 6, type = Type.U8)
     public final short proto;
 
-    @Field(order = 6, type = Type.U8)
+    @Field(order = 7, type = Type.U8)
     public final short dscp;
 
-    @Field(order = 7, type = Type.U8, padding = 3)
+    @Field(order = 8, type = Type.U8, padding = 3)
     public final short mask;
 
     private static final int SRC_IP_MASK = 0x1;
@@ -69,6 +71,7 @@
         return true;
     }
 
+    // TODO:  move to frameworks/libs/net and have this and BpfCoordinator import it.
     private byte[] toIpv4MappedAddressBytes(InetAddress ia) {
         final byte[] addr6 = new byte[16];
         if (ia != null) {
@@ -117,13 +120,12 @@
         return mask;
     }
 
-    // This constructor is necessary for BpfMap#getValue since all values must be
-    // in the constructor.
-    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
-            final int dstPortStart, final int dstPortEnd, final short proto,
+    private DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final long ifIndex,
+            final int srcPort, final int dstPortStart, final int dstPortEnd, final short proto,
             final short dscp) {
         this.src46 = toAddressField(src46);
         this.dst46 = toAddressField(dst46);
+        this.ifIndex = ifIndex;
 
         // These params need to be stored as 0 because uints are used in BpfMap.
         // If they are -1 BpfMap write will throw errors.
@@ -138,15 +140,15 @@
         this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
     }
 
-    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
-            final Range<Integer> dstPort, final short proto,
+    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final long ifIndex,
+            final int srcPort, final Range<Integer> dstPort, final short proto,
             final short dscp) {
-        this(src46, dst46, srcPort, dstPort != null ? dstPort.getLower() : -1,
+        this(src46, dst46, ifIndex, srcPort, dstPort != null ? dstPort.getLower() : -1,
                 dstPort != null ? dstPort.getUpper() : -1, proto, dscp);
     }
 
     public static final DscpPolicyValue NONE = new DscpPolicyValue(
-            null /* src46 */, null /* dst46 */, -1 /* srcPort */,
+            null /* src46 */, null /* dst46 */, 0 /* ifIndex */, -1 /* srcPort */,
             -1 /* dstPortStart */, -1 /* dstPortEnd */, (short) -1 /* proto */,
             (short) 0 /* dscp */);
 
@@ -170,9 +172,9 @@
 
         try {
             return String.format(
-                    "src46: %s, dst46: %s, srcPort: %d, dstPortStart: %d, dstPortEnd: %d,"
-                    + " protocol: %d, dscp %s", srcIpString, dstIpString, srcPort, dstPortStart,
-                    dstPortEnd, proto, dscp);
+                    "src46: %s, dst46: %s, ifIndex: %d, srcPort: %d, dstPortStart: %d,"
+                    + " dstPortEnd: %d, protocol: %d, dscp %s", srcIpString, dstIpString,
+                    ifIndex, srcPort, dstPortStart, dstPortEnd, proto, dscp);
         } catch (IllegalArgumentException e) {
             return String.format("String format error: " + e);
         }
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index aebb80d..b13ba93 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -21,8 +21,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkScore.KEEP_CONNECTED_NONE;
-import static android.net.NetworkScore.POLICY_EXITING;
-import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
 import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI;
 
 import android.annotation.IntDef;
@@ -31,8 +29,11 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkScore;
 import android.net.NetworkScore.KeepConnectedReason;
+import android.util.Log;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.MessageUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -46,6 +47,8 @@
  * they are handling a score that had the CS-managed bits set.
  */
 public class FullScore {
+    private static final String TAG = FullScore.class.getSimpleName();
+
     // This will be removed soon. Do *NOT* depend on it for any new code that is not part of
     // a migration.
     private final int mLegacyInt;
@@ -98,9 +101,17 @@
     /** @hide */
     public static final int POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD = 57;
 
+    // The network agent has communicated that this network no longer functions, and the underlying
+    // native network has been destroyed. The network will still be reported to clients as connected
+    // until a timeout expires, the agent disconnects, or the network no longer satisfies requests.
+    // This network should lose to an identical network that has not been destroyed, but should
+    // otherwise be scored exactly the same.
+    /** @hide */
+    public static final int POLICY_IS_DESTROYED = 56;
+
     // To help iterate when printing
     @VisibleForTesting
-    static final int MIN_CS_MANAGED_POLICY = POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
+    static final int MIN_CS_MANAGED_POLICY = POLICY_IS_DESTROYED;
     @VisibleForTesting
     static final int MAX_CS_MANAGED_POLICY = POLICY_IS_VALIDATED;
 
@@ -112,21 +123,22 @@
     private static final long EXTERNAL_POLICIES_MASK =
             0x00000000FFFFFFFFL & ~(1L << POLICY_YIELD_TO_BAD_WIFI);
 
+    private static SparseArray<String> sMessageNames = MessageUtils.findMessageNames(
+            new Class[]{FullScore.class, NetworkScore.class}, new String[]{"POLICY_"});
+
     @VisibleForTesting
     static @NonNull String policyNameOf(final int policy) {
-        switch (policy) {
-            case POLICY_IS_VALIDATED: return "IS_VALIDATED";
-            case POLICY_IS_VPN: return "IS_VPN";
-            case POLICY_EVER_USER_SELECTED: return "EVER_USER_SELECTED";
-            case POLICY_ACCEPT_UNVALIDATED: return "ACCEPT_UNVALIDATED";
-            case POLICY_IS_UNMETERED: return "IS_UNMETERED";
-            case POLICY_YIELD_TO_BAD_WIFI: return "YIELD_TO_BAD_WIFI";
-            case POLICY_TRANSPORT_PRIMARY: return "TRANSPORT_PRIMARY";
-            case POLICY_EXITING: return "EXITING";
-            case POLICY_IS_INVINCIBLE: return "INVINCIBLE";
-            case POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD: return "EVER_VALIDATED";
+        final String name = sMessageNames.get(policy);
+        if (name == null) {
+            // Don't throw here because name might be null due to proguard stripping out the
+            // POLICY_* constants, potentially causing a crash only on user builds because proguard
+            // does not run on userdebug builds.
+            // TODO: make MessageUtils safer by not returning the array and instead storing it
+            // internally and providing a getter (that does not throw) for individual values.
+            Log.wtf(TAG, "Unknown policy: " + policy);
+            return Integer.toString(policy);
         }
-        throw new IllegalArgumentException("Unknown policy : " + policy);
+        return name.substring("POLICY_".length());
     }
 
     // Bitmask of all the policies applied to this score.
@@ -149,6 +161,7 @@
      * @param config the NetworkAgentConfig of the network
      * @param everValidated whether this network has ever validated
      * @param yieldToBadWiFi whether this network yields to a previously validated wifi gone bad
+     * @param destroyed whether this network has been destroyed pending a replacement connecting
      * @return a FullScore that is appropriate to use for ranking.
      */
     // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the
@@ -156,7 +169,7 @@
     // connectivity for backward compatibility.
     public static FullScore fromNetworkScore(@NonNull final NetworkScore score,
             @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config,
-            final boolean everValidated, final boolean yieldToBadWiFi) {
+            final boolean everValidated, final boolean yieldToBadWiFi, final boolean destroyed) {
         return withPolicies(score.getLegacyInt(), score.getPolicies(),
                 score.getKeepConnectedReason(),
                 caps.hasCapability(NET_CAPABILITY_VALIDATED),
@@ -166,6 +179,7 @@
                 config.explicitlySelected,
                 config.acceptUnvalidated,
                 yieldToBadWiFi,
+                destroyed,
                 false /* invincible */); // only prospective scores can be invincible
     }
 
@@ -174,7 +188,7 @@
      *
      * NetworkOffers have score filters that are compared to the scores of actual networks
      * to see if they could possibly beat the current satisfier. Some things the agent can't
-     * know in advance ; a good example is the validation bit – some networks will validate,
+     * know in advance; a good example is the validation bit – some networks will validate,
      * others won't. For comparison purposes, assume the best, so all possibly beneficial
      * networks will be brought up.
      *
@@ -197,12 +211,14 @@
         final boolean everUserSelected = false;
         // Don't assume the user will accept unvalidated connectivity.
         final boolean acceptUnvalidated = false;
+        // A network can only be destroyed once it has connected.
+        final boolean destroyed = false;
         // A prospective score is invincible if the legacy int in the filter is over the maximum
         // score.
         final boolean invincible = score.getLegacyInt() > NetworkRanker.LEGACY_INT_MAX;
         return withPolicies(score.getLegacyInt(), score.getPolicies(), KEEP_CONNECTED_NONE,
                 mayValidate, vpn, unmetered, everValidated, everUserSelected, acceptUnvalidated,
-                yieldToBadWiFi, invincible);
+                yieldToBadWiFi, destroyed, invincible);
     }
 
     /**
@@ -218,7 +234,8 @@
     public FullScore mixInScore(@NonNull final NetworkCapabilities caps,
             @NonNull final NetworkAgentConfig config,
             final boolean everValidated,
-            final boolean yieldToBadWifi) {
+            final boolean yieldToBadWifi,
+            final boolean destroyed) {
         return withPolicies(mLegacyInt, mPolicies, mKeepConnectedReason,
                 caps.hasCapability(NET_CAPABILITY_VALIDATED),
                 caps.hasTransport(TRANSPORT_VPN),
@@ -227,6 +244,7 @@
                 config.explicitlySelected,
                 config.acceptUnvalidated,
                 yieldToBadWifi,
+                destroyed,
                 false /* invincible */); // only prospective scores can be invincible
     }
 
@@ -243,6 +261,7 @@
             final boolean everUserSelected,
             final boolean acceptUnvalidated,
             final boolean yieldToBadWiFi,
+            final boolean destroyed,
             final boolean invincible) {
         return new FullScore(legacyInt, (externalPolicies & EXTERNAL_POLICIES_MASK)
                 | (isValidated       ? 1L << POLICY_IS_VALIDATED : 0)
@@ -252,6 +271,7 @@
                 | (everUserSelected  ? 1L << POLICY_EVER_USER_SELECTED : 0)
                 | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0)
                 | (yieldToBadWiFi    ? 1L << POLICY_YIELD_TO_BAD_WIFI : 0)
+                | (destroyed         ? 1L << POLICY_IS_DESTROYED : 0)
                 | (invincible        ? 1L << POLICY_IS_INVINCIBLE : 0),
                 keepConnectedReason);
     }
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index c66a280..7b06682 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -132,8 +132,8 @@
         final boolean skip464xlat = (nai.netAgentConfig() != null)
                 && nai.netAgentConfig().skip464xlat;
 
-        return supported && connected && isIpv6OnlyNetwork && !skip464xlat
-            && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
+        return supported && connected && isIpv6OnlyNetwork && !skip464xlat && !nai.destroyed
+                && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
                 ? isCellular464XlatEnabled() : true);
     }
 
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index e917b3f..1fc5a8f 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -106,6 +106,12 @@
 //       or tunnel) but does not disconnect from the AP/cell tower, or
 //    d. a stand-alone device offering a WiFi AP without an uplink for configuration purposes.
 // 5. registered, created, connected, validated
+// 6. registered, created, connected, (validated or unvalidated), destroyed
+//    This is an optional state where the underlying native network is destroyed but the network is
+//    still connected for scoring purposes, so can satisfy requests, including the default request.
+//    It is used when the transport layer wants to replace a network with another network (e.g.,
+//    when Wi-Fi has roamed to a different BSSID that is part of a different L3 network) and does
+//    not want the device to switch to another network until the replacement connects and validates.
 //
 // The device's default network connection:
 // ----------------------------------------
@@ -184,6 +190,11 @@
     // shows up in API calls, is able to satisfy NetworkRequests and can become the default network.
     // This is a sticky bit; once set it is never cleared.
     public boolean everConnected;
+    // Whether this network has been destroyed and is being kept temporarily until it is replaced.
+    public boolean destroyed;
+    // To check how long it has been since last roam.
+    public long lastRoamTimestamp;
+
     // Set to true if this Network successfully passed validation or if it did not satisfy the
     // default NetworkRequest in which case validation will not be attempted.
     // This is a sticky bit; once set it is never cleared even if future validation attempts fail.
@@ -723,6 +734,12 @@
             mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES,
                     new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
         }
+
+        @Override
+        public void sendUnregisterAfterReplacement(final int timeoutMillis) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_UNREGISTER_AFTER_REPLACEMENT,
+                    new Pair<>(NetworkAgentInfo.this, timeoutMillis)).sendToTarget();
+        }
     }
 
     /**
@@ -746,7 +763,7 @@
         final NetworkCapabilities oldNc = networkCapabilities;
         networkCapabilities = nc;
         mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidatedForYield(),
-                yieldToBadWiFi());
+                yieldToBadWiFi(), destroyed);
         final NetworkMonitorManager nm = mNetworkMonitor;
         if (nm != null) {
             nm.notifyNetworkCapabilitiesChanged(nc);
@@ -874,7 +891,7 @@
 
     /**
      * Returns the number of requests currently satisfied by this network of type
-     * {@link android.net.NetworkRequest.Type.BACKGROUND_REQUEST}.
+     * {@link android.net.NetworkRequest.Type#BACKGROUND_REQUEST}.
      */
     public int numBackgroundNetworkRequests() {
         return mNumBackgroundNetworkRequests;
@@ -961,17 +978,17 @@
      */
     public void setScore(final NetworkScore score) {
         mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig,
-                everValidatedForYield(), yieldToBadWiFi());
+                everValidatedForYield(), yieldToBadWiFi(), destroyed);
     }
 
     /**
      * Update the ConnectivityService-managed bits in the score.
      *
-     * Call this after updating the network agent config.
+     * Call this after changing any data that might affect the score (e.g., agent config).
      */
     public void updateScoreForNetworkAgentUpdate() {
         mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig,
-                everValidatedForYield(), yieldToBadWiFi());
+                everValidatedForYield(), yieldToBadWiFi(), destroyed);
     }
 
     private boolean everValidatedForYield() {
@@ -1019,7 +1036,7 @@
      * when a network is newly created.
      *
      * @param requestId The requestId of the request that no longer need to be served by this
-     *                  network. Or {@link NetworkRequest.REQUEST_ID_NONE} if this is the
+     *                  network. Or {@link NetworkRequest#REQUEST_ID_NONE} if this is the
      *                  {@code InactivityTimer} for a newly created network.
      */
     // TODO: Consider creating a dedicated function for nascent network, e.g. start/stopNascent.
@@ -1204,16 +1221,16 @@
         if (nc.hasTransport(TRANSPORT_TEST)) {
             nc.restrictCapabilitiesForTestNetwork(creatorUid);
         }
-        if (!areAccessUidsAcceptableFromNetworkAgent(nc, authenticator)) {
-            nc.setAccessUids(new ArraySet<>());
+        if (!areAllowedUidsAcceptableFromNetworkAgent(nc, authenticator)) {
+            nc.setAllowedUids(new ArraySet<>());
         }
     }
 
-    private static boolean areAccessUidsAcceptableFromNetworkAgent(
+    private static boolean areAllowedUidsAcceptableFromNetworkAgent(
             @NonNull final NetworkCapabilities nc,
             @Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
         // NCs without access UIDs are fine.
-        if (!nc.hasAccessUids()) return true;
+        if (!nc.hasAllowedUids()) return true;
         // S and below must never accept access UIDs, even if an agent sends them, because netd
         // didn't support the required feature in S.
         if (!SdkLevel.isAtLeastT()) return false;
@@ -1229,9 +1246,9 @@
         // This can only work in T where there is support for CarrierPrivilegeAuthenticator
         if (null != carrierPrivilegeAuthenticator
                 && nc.hasSingleTransport(TRANSPORT_CELLULAR)
-                && (1 == nc.getAccessUidsNoCopy().size())
+                && (1 == nc.getAllowedUidsNoCopy().size())
                 && (carrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
-                        nc.getAccessUidsNoCopy().valueAt(0), nc))) {
+                        nc.getAllowedUidsNoCopy().valueAt(0), nc))) {
             return true;
         }
 
@@ -1247,6 +1264,8 @@
                 + "network{" + network + "}  handle{" + network.getNetworkHandle() + "}  ni{"
                 + networkInfo.toShortString() + "} "
                 + mScore + " "
+                + (created ? " created" : "")
+                + (destroyed ? " destroyed" : "")
                 + (isNascent() ? " nascent" : (isLingering() ? " lingering" : ""))
                 + (everValidated ? " everValidated" : "")
                 + (lastValidated ? " lastValidated" : "")
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index 2e51be3..509110d 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -206,7 +206,7 @@
         }
 
         for (RouteInfo route : mLinkProperties.getRoutes()) {
-            if (route.hasGateway()) {
+            if (route.getType() == RouteInfo.RTN_UNICAST && route.hasGateway()) {
                 InetAddress gateway = route.getGateway();
                 prepareIcmpMeasurement(gateway);
                 if (route.isIPv6Default()) {
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
index 43da1d0..babc353 100644
--- a/service/src/com/android/server/connectivity/NetworkRanker.java
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -28,6 +28,7 @@
 import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED;
 import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED;
 import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
+import static com.android.server.connectivity.FullScore.POLICY_IS_DESTROYED;
 import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE;
 import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED;
 import static com.android.server.connectivity.FullScore.POLICY_IS_VPN;
@@ -263,6 +264,15 @@
             }
         }
 
+        // If two networks are equivalent, and one has been destroyed pending replacement, keep the
+        // other one. This ensures that when the replacement connects, it's preferred.
+        partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_IS_DESTROYED),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) {
+            candidates = new ArrayList<>(accepted);
+        }
+
         // At this point there are still multiple networks passing all the tests above. If any
         // of them is the previous satisfier, keep it.
         if (candidates.contains(currentSatisfier)) return currentSatisfier;
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index ac46054..62b3add 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -52,6 +52,7 @@
 import android.net.Uri;
 import android.net.util.SharedLog;
 import android.os.Build;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.os.SystemConfigManager;
@@ -66,7 +67,10 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
+import com.android.networkstack.apishim.ProcessShimImpl;
+import com.android.networkstack.apishim.common.ProcessShim;
 import com.android.server.BpfNetMaps;
 
 import java.util.ArrayList;
@@ -95,6 +99,8 @@
     private final Context mContext;
     private final BpfNetMaps mBpfNetMaps;
 
+    private static final ProcessShim sProcessShim = ProcessShimImpl.newInstance();
+
     @GuardedBy("this")
     private final Set<UserHandle> mUsers = new HashSet<>();
 
@@ -121,9 +127,17 @@
     @GuardedBy("this")
     private final Set<Integer> mUidsAllowedOnRestrictedNetworks = new ArraySet<>();
 
+    // Store PackageManager for each user.
+    // Keys are users, Values are PackageManagers which get from each user.
     @GuardedBy("this")
     private final Map<UserHandle, PackageManager> mUsersPackageManager = new ArrayMap<>();
 
+    // Store appIds traffic permissions for each user.
+    // Keys are users, Values are SparseArrays where each entry maps an appId to the permissions
+    // that appId has within that user.
+    @GuardedBy("this")
+    private final Map<UserHandle, SparseIntArray> mUsersTrafficPermissions = new ArrayMap<>();
+
     private static final int SYSTEM_APPID = SYSTEM_UID;
 
     private static final int MAX_PERMISSION_UPDATE_LOGS = 40;
@@ -235,6 +249,10 @@
         }
     }
 
+    private static boolean hasSdkSandbox(final int uid) {
+        return SdkLevel.isAtLeastT() && Process.isApplicationUid(uid);
+    }
+
     // Return the network permission for the passed list of apps. Note that this depends on the
     // current settings of the device (See isUidAllowedOnRestrictedNetworks).
     private SparseIntArray makeUidsNetworkPerm(final List<PackageInfo> apps) {
@@ -247,6 +265,10 @@
             final int permission = getPackageNetdNetworkPermission(app);
             if (isHigherNetworkPermission(permission, uidsPerm.get(uid, PERMISSION_NONE))) {
                 uidsPerm.put(uid, permission);
+                if (hasSdkSandbox(uid)) {
+                    int sdkSandboxUid = sProcessShim.toSdkSandboxUid(uid);
+                    uidsPerm.put(sdkSandboxUid, permission);
+                }
             }
         }
         return uidsPerm;
@@ -262,7 +284,11 @@
             }
             final int otherNetdPerms = getNetdPermissionMask(app.requestedPermissions,
                     app.requestedPermissionsFlags);
-            appIdsPerm.put(appId, appIdsPerm.get(appId) | otherNetdPerms);
+            final int permission = appIdsPerm.get(appId) | otherNetdPerms;
+            appIdsPerm.put(appId, permission);
+            if (hasSdkSandbox(appId)) {
+                appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
+            }
         }
         return appIdsPerm;
     }
@@ -274,25 +300,43 @@
         sendUidsNetworkPermission(uids, true /* add */);
     }
 
-    private void updateAppIdsTrafficPermission(final SparseIntArray appIds,
-            final SparseIntArray extraAppIds) {
-        for (int i = 0; i < extraAppIds.size(); i++) {
-            final int appId = extraAppIds.keyAt(i);
-            final int permission = extraAppIds.valueAt(i);
-            appIds.put(appId, appIds.get(appId) | permission);
+    /**
+     * Calculates permissions for appIds.
+     * Maps each appId to the union of all traffic permissions that the appId has in all users.
+     *
+     * @return The appIds traffic permissions.
+     */
+    private synchronized SparseIntArray makeAppIdsTrafficPermForAllUsers() {
+        final SparseIntArray appIds = new SparseIntArray();
+        // Check appIds permissions from each user.
+        for (UserHandle user : mUsersTrafficPermissions.keySet()) {
+            final SparseIntArray userAppIds = mUsersTrafficPermissions.get(user);
+            for (int i = 0; i < userAppIds.size(); i++) {
+                final int appId = userAppIds.keyAt(i);
+                final int permission = userAppIds.valueAt(i);
+                appIds.put(appId, appIds.get(appId) | permission);
+            }
         }
-        sendAppIdsTrafficPermission(appIds);
+        return appIds;
     }
 
     private SparseIntArray getSystemTrafficPerm() {
         final SparseIntArray appIdsPerm = new SparseIntArray();
         for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {
             final int appId = UserHandle.getAppId(uid);
-            appIdsPerm.put(appId, appIdsPerm.get(appId) | PERMISSION_INTERNET);
+            final int permission = appIdsPerm.get(appId) | PERMISSION_INTERNET;
+            appIdsPerm.put(appId, permission);
+            if (hasSdkSandbox(appId)) {
+                appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
+            }
         }
         for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {
             final int appId = UserHandle.getAppId(uid);
-            appIdsPerm.put(appId, appIdsPerm.get(appId) | PERMISSION_UPDATE_DEVICE_STATS);
+            final int permission = appIdsPerm.get(appId) | PERMISSION_UPDATE_DEVICE_STATS;
+            appIdsPerm.put(appId, permission);
+            if (hasSdkSandbox(appId)) {
+                appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
+            }
         }
         return appIdsPerm;
     }
@@ -337,6 +381,10 @@
         // mUidsAllowedOnRestrictedNetworks.
         updateUidsAllowedOnRestrictedNetworks(mDeps.getUidsAllowedOnRestrictedNetworks(mContext));
 
+        // Read system traffic permissions when a user removed and put them to USER_ALL because they
+        // are not specific to any particular user.
+        mUsersTrafficPermissions.put(UserHandle.ALL, getSystemTrafficPerm());
+
         final List<UserHandle> usrs = mUserManager.getUserHandles(true /* excludeDying */);
         // Update netd permissions for all users.
         for (UserHandle user : usrs) {
@@ -461,9 +509,16 @@
         final SparseIntArray uids = makeUidsNetworkPerm(apps);
         updateUidsNetworkPermission(uids);
 
-        // App ids traffic permission
-        final SparseIntArray appIds = makeAppIdsTrafficPerm(apps);
-        updateAppIdsTrafficPermission(appIds, getSystemTrafficPerm());
+        // Add new user appIds permissions.
+        final SparseIntArray addedUserAppIds = makeAppIdsTrafficPerm(apps);
+        mUsersTrafficPermissions.put(user, addedUserAppIds);
+        // Generate appIds from all users and send result to netd.
+        final SparseIntArray appIds = makeAppIdsTrafficPermForAllUsers();
+        sendAppIdsTrafficPermission(appIds);
+
+        // Log user added
+        mPermissionUpdateLogs.log("New user(" + user.getIdentifier() + ") added: nPerm uids="
+                + uids + ", tPerm appIds=" + addedUserAppIds);
     }
 
     /**
@@ -476,6 +531,7 @@
     public synchronized void onUserRemoved(@NonNull UserHandle user) {
         mUsers.remove(user);
 
+        // Remove uids network permissions that belongs to the user.
         final SparseIntArray removedUids = new SparseIntArray();
         final SparseIntArray allUids = mUidToNetworkPerm.clone();
         for (int i = 0; i < allUids.size(); i++) {
@@ -486,6 +542,27 @@
             }
         }
         sendUidsNetworkPermission(removedUids, false /* add */);
+
+        // Remove appIds traffic permission that belongs to the user
+        final SparseIntArray removedUserAppIds = mUsersTrafficPermissions.remove(user);
+        // Generate appIds from left users.
+        final SparseIntArray appIds = makeAppIdsTrafficPermForAllUsers();
+        // Clear permission on those appIds belong to this user only, set the permission to
+        // PERMISSION_UNINSTALLED.
+        if (removedUserAppIds != null) {
+            for (int i = 0; i < removedUserAppIds.size(); i++) {
+                final int appId = removedUserAppIds.keyAt(i);
+                // Need to clear permission if the removed appId is not found in the array.
+                if (appIds.indexOfKey(appId) < 0) {
+                    appIds.put(appId, PERMISSION_UNINSTALLED);
+                }
+            }
+        }
+        sendAppIdsTrafficPermission(appIds);
+
+        // Log user removed
+        mPermissionUpdateLogs.log("User(" + user.getIdentifier() + ") removed: nPerm uids="
+                + removedUids + ", tPerm appIds=" + removedUserAppIds);
     }
 
     /**
@@ -572,6 +649,39 @@
         }
     }
 
+    private synchronized void updateAppIdTrafficPermission(int uid) {
+        final int appId = UserHandle.getAppId(uid);
+        final int uidTrafficPerm = getTrafficPermissionForUid(uid);
+        final SparseIntArray userTrafficPerms =
+                mUsersTrafficPermissions.get(UserHandle.getUserHandleForUid(uid));
+        if (userTrafficPerms == null) {
+            Log.wtf(TAG, "Can't get user traffic permission from uid=" + uid);
+            return;
+        }
+        // Do not put PERMISSION_UNINSTALLED into the array. If no package left on the uid
+        // (PERMISSION_UNINSTALLED), remove the appId from the array. Otherwise, update the latest
+        // permission to the appId.
+        if (uidTrafficPerm == PERMISSION_UNINSTALLED) {
+            userTrafficPerms.delete(appId);
+        } else {
+            userTrafficPerms.put(appId, uidTrafficPerm);
+        }
+    }
+
+    private synchronized int getAppIdTrafficPermission(int appId) {
+        int permission = PERMISSION_NONE;
+        boolean installed = false;
+        for (UserHandle user : mUsersTrafficPermissions.keySet()) {
+            final SparseIntArray userApps = mUsersTrafficPermissions.get(user);
+            final int appIdx = userApps.indexOfKey(appId);
+            if (appIdx >= 0) {
+                permission |= userApps.valueAt(appIdx);
+                installed = true;
+            }
+        }
+        return installed ? permission : PERMISSION_UNINSTALLED;
+    }
+
     /**
      * Called when a package is added.
      *
@@ -581,9 +691,12 @@
      * @hide
      */
     public synchronized void onPackageAdded(@NonNull final String packageName, final int uid) {
+        // Update uid permission.
+        updateAppIdTrafficPermission(uid);
+        // Get the appId permission from all users then send the latest permission to netd.
         final int appId = UserHandle.getAppId(uid);
-        final int trafficPerm = getTrafficPermissionForUid(uid);
-        sendPackagePermissionsForAppId(appId, trafficPerm);
+        final int appIdTrafficPerm = getAppIdTrafficPermission(appId);
+        sendPackagePermissionsForAppId(appId, appIdTrafficPerm);
 
         final int currentPermission = mUidToNetworkPerm.get(uid, PERMISSION_NONE);
         final int permission = highestPermissionForUid(uid, currentPermission, packageName);
@@ -592,6 +705,12 @@
 
             SparseIntArray apps = new SparseIntArray();
             apps.put(uid, permission);
+
+            if (hasSdkSandbox(uid)) {
+                int sdkSandboxUid = sProcessShim.toSdkSandboxUid(uid);
+                mUidToNetworkPerm.put(sdkSandboxUid, permission);
+                apps.put(sdkSandboxUid, permission);
+            }
             sendUidsNetworkPermission(apps, true /* add */);
         }
 
@@ -601,10 +720,12 @@
         // package can bypass VPN.
         updateVpnUid(uid, true /* add */);
         mAllApps.add(appId);
+
+        // Log package added.
         mPermissionUpdateLogs.log("Package add: name=" + packageName + ", uid=" + uid
                 + ", nPerm=(" + permissionToString(permission) + "/"
                 + permissionToString(currentPermission) + ")"
-                + ", tPerm=" + permissionToString(trafficPerm));
+                + ", tPerm=" + permissionToString(appIdTrafficPerm));
     }
 
     private int highestUidNetworkPermission(int uid) {
@@ -632,9 +753,12 @@
      * @hide
      */
     public synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) {
+        // Update uid permission.
+        updateAppIdTrafficPermission(uid);
+        // Get the appId permission from all users then send the latest permission to netd.
         final int appId = UserHandle.getAppId(uid);
-        final int trafficPerm = getTrafficPermissionForUid(uid);
-        sendPackagePermissionsForAppId(appId, trafficPerm);
+        final int appIdTrafficPerm = getAppIdTrafficPermission(appId);
+        sendPackagePermissionsForAppId(appId, appIdTrafficPerm);
 
         // If the newly-removed package falls within some VPN's uid range, update Netd with it.
         // This needs to happen before the mUidToNetworkPerm update below, since
@@ -648,19 +772,34 @@
 
         final int currentPermission = mUidToNetworkPerm.get(uid, PERMISSION_NONE);
         final int permission = highestUidNetworkPermission(uid);
+
+        // Log package removed.
         mPermissionUpdateLogs.log("Package remove: name=" + packageName + ", uid=" + uid
                 + ", nPerm=(" + permissionToString(permission) + "/"
                 + permissionToString(currentPermission) + ")"
-                + ", tPerm=" + permissionToString(trafficPerm));
+                + ", tPerm=" + permissionToString(appIdTrafficPerm));
+
         if (permission != currentPermission) {
             final SparseIntArray apps = new SparseIntArray();
+            int sdkSandboxUid = -1;
+            if (hasSdkSandbox(uid)) {
+                sdkSandboxUid = sProcessShim.toSdkSandboxUid(uid);
+            }
             if (permission == PERMISSION_NONE) {
                 mUidToNetworkPerm.delete(uid);
                 apps.put(uid, PERMISSION_NETWORK);  // doesn't matter which permission we pick here
+                if (sdkSandboxUid != -1) {
+                    mUidToNetworkPerm.delete(sdkSandboxUid);
+                    apps.put(sdkSandboxUid, PERMISSION_NETWORK);
+                }
                 sendUidsNetworkPermission(apps, false);
             } else {
                 mUidToNetworkPerm.put(uid, permission);
                 apps.put(uid, permission);
+                if (sdkSandboxUid != -1) {
+                    mUidToNetworkPerm.put(sdkSandboxUid, permission);
+                    apps.put(sdkSandboxUid, permission);
+                }
                 sendUidsNetworkPermission(apps, true);
             }
         }
@@ -828,6 +967,10 @@
     void sendPackagePermissionsForAppId(int appId, int permissions) {
         SparseIntArray netdPermissionsAppIds = new SparseIntArray();
         netdPermissionsAppIds.put(appId, permissions);
+        if (hasSdkSandbox(appId)) {
+            int sdkSandboxAppId = sProcessShim.toSdkSandboxUid(appId);
+            netdPermissionsAppIds.put(sdkSandboxAppId, permissions);
+        }
         sendAppIdsTrafficPermission(netdPermissionsAppIds);
     }
 
@@ -925,9 +1068,19 @@
                 // Doesn't matter which permission is set here.
                 removedUids.put(uid, PERMISSION_NETWORK);
                 mUidToNetworkPerm.delete(uid);
+                if (hasSdkSandbox(uid)) {
+                    int sdkSandboxUid = sProcessShim.toSdkSandboxUid(uid);
+                    removedUids.put(sdkSandboxUid, PERMISSION_NETWORK);
+                    mUidToNetworkPerm.delete(sdkSandboxUid);
+                }
             } else {
                 updatedUids.put(uid, permission);
                 mUidToNetworkPerm.put(uid, permission);
+                if (hasSdkSandbox(uid)) {
+                    int sdkSandboxUid = sProcessShim.toSdkSandboxUid(uid);
+                    updatedUids.put(sdkSandboxUid, permission);
+                    mUidToNetworkPerm.put(sdkSandboxUid, permission);
+                }
             }
         }
 
diff --git a/service/src/com/android/server/connectivity/TcpKeepaliveController.java b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
index acfbb3c..a9cb2fa 100644
--- a/service/src/com/android/server/connectivity/TcpKeepaliveController.java
+++ b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
@@ -124,7 +124,12 @@
             final TcpKeepalivePacketDataParcelable tcpDetails = switchToRepairMode(fd);
             // TODO: consider building a TcpKeepalivePacketData directly from switchToRepairMode
             return fromStableParcelable(tcpDetails);
-        } catch (InvalidPacketException | InvalidSocketException e) {
+        // Use separate catch blocks: a combined catch would get wrongly optimized by R8
+        // (b/226127213).
+        } catch (InvalidSocketException e) {
+            switchOutOfRepairMode(fd);
+            throw e;
+        } catch (InvalidPacketException e) {
             switchOutOfRepairMode(fd);
             throw e;
         }
diff --git a/service/src/com/android/server/connectivity/UidRangeUtils.java b/service/src/com/android/server/connectivity/UidRangeUtils.java
index 7318296..541340b 100644
--- a/service/src/com/android/server/connectivity/UidRangeUtils.java
+++ b/service/src/com/android/server/connectivity/UidRangeUtils.java
@@ -21,6 +21,7 @@
 import android.util.ArraySet;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -125,7 +126,7 @@
     }
 
     /**
-     * Convert a list of uid to set of UidRanges.
+     * Convert a list of uids to set of UidRanges.
      * @param uids list of uids
      * @return set of UidRanges
      * @hide
@@ -153,4 +154,34 @@
         uidRangeSet.add(new UidRange(start, stop));
         return uidRangeSet;
     }
+
+    /**
+     * Convert an array of uids to set of UidRanges.
+     * @param uids array of uids
+     * @return set of UidRanges
+     * @hide
+     */
+    public static ArraySet<UidRange> convertArrayToUidRange(@NonNull int[] uids) {
+        Objects.requireNonNull(uids);
+        final ArraySet<UidRange> uidRangeSet = new ArraySet<UidRange>();
+        if (uids.length == 0) {
+            return uidRangeSet;
+        }
+        int[] uidsNew = uids.clone();
+        Arrays.sort(uidsNew);
+        int start = uidsNew[0];
+        int stop = start;
+
+        for (int i : uidsNew) {
+            if (i <= stop + 1) {
+                stop = i;
+            } else {
+                uidRangeSet.add(new UidRange(start, stop));
+                start = i;
+                stop = i;
+            }
+        }
+        uidRangeSet.add(new UidRange(start, stop));
+        return uidRangeSet;
+    }
 }
diff --git a/service/src/com/android/server/net/DelayedDiskWrite.java b/service/src/com/android/server/net/DelayedDiskWrite.java
new file mode 100644
index 0000000..35dc455
--- /dev/null
+++ b/service/src/com/android/server/net/DelayedDiskWrite.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * This class provides APIs to do a delayed data write to a given {@link OutputStream}.
+ */
+public class DelayedDiskWrite {
+    private static final String TAG = "DelayedDiskWrite";
+
+    private HandlerThread mDiskWriteHandlerThread;
+    private Handler mDiskWriteHandler;
+    /* Tracks multiple writes on the same thread */
+    private int mWriteSequence = 0;
+
+    /**
+     * Used to do a delayed data write to a given {@link OutputStream}.
+     */
+    public interface Writer {
+        /**
+         * write data to a given {@link OutputStream}.
+         */
+        void onWriteCalled(DataOutputStream out) throws IOException;
+    }
+
+    /**
+     * Do a delayed data write to a given output stream opened from filePath.
+     */
+    public void write(final String filePath, final Writer w) {
+        write(filePath, w, true);
+    }
+
+    /**
+     * Do a delayed data write to a given output stream opened from filePath.
+     */
+    public void write(final String filePath, final Writer w, final boolean open) {
+        if (TextUtils.isEmpty(filePath)) {
+            throw new IllegalArgumentException("empty file path");
+        }
+
+        /* Do a delayed write to disk on a separate handler thread */
+        synchronized (this) {
+            if (++mWriteSequence == 1) {
+                mDiskWriteHandlerThread = new HandlerThread("DelayedDiskWriteThread");
+                mDiskWriteHandlerThread.start();
+                mDiskWriteHandler = new Handler(mDiskWriteHandlerThread.getLooper());
+            }
+        }
+
+        mDiskWriteHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                doWrite(filePath, w, open);
+            }
+        });
+    }
+
+    private void doWrite(String filePath, Writer w, boolean open) {
+        DataOutputStream out = null;
+        try {
+            if (open) {
+                out = new DataOutputStream(new BufferedOutputStream(
+                        new FileOutputStream(filePath)));
+            }
+            w.onWriteCalled(out);
+        } catch (IOException e) {
+            loge("Error writing data file " + filePath);
+        } finally {
+            if (out != null) {
+                try {
+                    out.close();
+                } catch (Exception e) { }
+            }
+
+            // Quit if no more writes sent
+            synchronized (this) {
+                if (--mWriteSequence == 0) {
+                    mDiskWriteHandler.getLooper().quit();
+                    mDiskWriteHandler = null;
+                    mDiskWriteHandlerThread = null;
+                }
+            }
+        }
+    }
+
+    private void loge(String s) {
+        Log.e(TAG, s);
+    }
+}
+
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 8782684..509e881 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -36,6 +36,7 @@
         "modules-utils-build",
         "net-tests-utils",
         "net-utils-framework-common",
+        "platform-compat-test-rules",
         "platform-test-annotations",
     ],
     libs: [
@@ -43,10 +44,23 @@
     ],
 }
 
-// Connectivity coverage tests combines Tethering and Connectivity tests, each with their
-// respective jarjar rules applied.
-// Some tests may be duplicated (in particular static lib tests), as they need to be run under both
-// jarjared packages to cover both usages.
+// Combine Connectivity, NetworkStack and Tethering jarjar rules for coverage target.
+// The jarjar files are simply concatenated in the order specified in srcs.
+// jarjar stops at the first matching rule, so order of concatenation affects the output.
+genrule {
+    name: "ConnectivityCoverageJarJarRules",
+    srcs: [
+        "tethering-jni-jarjar-rules.txt",
+        ":connectivity-jarjar-rules",
+        ":TetheringTestsJarJarRules",
+        ":NetworkStackJarJarRules",
+    ],
+    out: ["jarjar-rules-connectivity-coverage.txt"],
+    // Concat files with a line break in the middle
+    cmd: "for src in $(in); do cat $${src}; echo; done > $(out)",
+    visibility: ["//visibility:private"],
+}
+
 android_library {
     name: "ConnectivityCoverageTestsLib",
     min_sdk_version: "30",
@@ -54,8 +68,11 @@
         "FrameworksNetTestsLib",
         "NetdStaticLibTestsLib",
         "NetworkStaticLibTestsLib",
+        "NetworkStackTestsLib",
+        "TetheringTestsLatestSdkLib",
+        "TetheringIntegrationTestsLatestSdkLib",
     ],
-    jarjar_rules: ":connectivity-jarjar-rules",
+    jarjar_rules: ":ConnectivityCoverageJarJarRules",
     manifest: "AndroidManifest_coverage.xml",
     visibility: ["//visibility:private"],
 }
@@ -80,7 +97,6 @@
         "mockito-target-extended-minus-junit4",
         "modules-utils-native-coverage-listener",
         "ConnectivityCoverageTestsLib",
-        "TetheringCoverageTestsLib",
     ],
     jni_libs: [
         // For mockito extended
@@ -115,7 +131,7 @@
         // meaning @hide APIs in framework-connectivity are resolved before @SystemApi
         // stubs in framework
         "framework-connectivity.impl",
-        "framework-connectivity-tiramisu.impl",
+        "framework-connectivity-t.impl",
         "framework-tethering.impl",
         "framework",
 
diff --git a/tests/common/AndroidTest_Coverage.xml b/tests/common/AndroidTest_Coverage.xml
index 7c8e710..d4898b2 100644
--- a/tests/common/AndroidTest_Coverage.xml
+++ b/tests/common/AndroidTest_Coverage.xml
@@ -18,6 +18,8 @@
     </target_preparer>
 
     <option name="test-tag" value="ConnectivityCoverageTests" />
+    <!-- Tethering/Connectivity is a SDK 30+ module -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.connectivity.tests.coverage" />
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 4d85a57..8fc636a 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -20,6 +20,7 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
 import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
 
@@ -30,6 +31,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.compat.testing.PlatformCompatChangeRule;
 import android.net.LinkProperties.ProvisioningChange;
 import android.os.Build;
 import android.system.OsConstants;
@@ -45,6 +47,9 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -65,6 +70,9 @@
     @Rule
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
+    @Rule
+    public final PlatformCompatChangeRule compatChangeRule = new PlatformCompatChangeRule();
+
     private static final InetAddress ADDRV4 = address("75.208.6.1");
     private static final InetAddress ADDRV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
     private static final InetAddress DNS1 = address("75.208.7.1");
@@ -1254,6 +1262,7 @@
     }
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("wlan0");
@@ -1268,4 +1277,36 @@
         lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460));
         assertEquals(2, lp.getRoutes().size());
     }
+
+    @Test @IgnoreUpTo(SC_V2)
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+    public void testExcludedRoutesEnabled() {
+        final LinkProperties lp = new LinkProperties();
+        assertEquals(0, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 0), RTN_UNREACHABLE));
+        assertEquals(1, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 0), RTN_THROW));
+        assertEquals(2, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(GATEWAY1));
+        assertEquals(3, lp.getRoutes().size());
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    @DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+    public void testExcludedRoutesDisabled() {
+        final LinkProperties lp = new LinkProperties();
+        assertEquals(0, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 0), RTN_UNREACHABLE));
+        assertEquals(0, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 5), RTN_THROW));
+        assertEquals(0, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 2), RTN_UNICAST));
+        assertEquals(1, lp.getRoutes().size());
+    }
 }
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index e5db09f..c05cdbd 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -88,7 +88,7 @@
         assertEquals("TEST_NETWORK", config.getLegacyTypeName())
         if (isAtLeastT()) {
             assertTrue(config.areLocalRoutesExcludedForVpn())
-            assertTrue(config.getVpnRequiresValidation())
+            assertTrue(config.isVpnValidationRequired())
         }
         if (isAtLeastS()) {
             assertEquals(testExtraInfo, config.getLegacyExtraInfo())
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index b6926a8..c30e1d3 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -82,12 +82,11 @@
 import android.util.ArraySet;
 import android.util.Range;
 
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.testutils.CompatUtil;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -99,8 +98,12 @@
 import java.util.List;
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
 @SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+// NetworkCapabilities is only updatable on S+, and this test covers behavior which implementation
+// is self-contained within NetworkCapabilities.java, so it does not need to be run on, or
+// compatible with, earlier releases.
+@IgnoreUpTo(Build.VERSION_CODES.R)
 @ConnectivityModuleTest
 public class NetworkCapabilitiesTest {
     private static final String TEST_SSID = "TEST_SSID";
@@ -310,38 +313,38 @@
     }
 
     @Test @IgnoreUpTo(SC_V2)
-    public void testSetAccessUids() {
+    public void testSetAllowedUids() {
         final NetworkCapabilities nc = new NetworkCapabilities();
-        assertThrows(NullPointerException.class, () -> nc.setAccessUids(null));
-        assertFalse(nc.hasAccessUids());
-        assertFalse(nc.isAccessUid(0));
-        assertFalse(nc.isAccessUid(1000));
-        assertEquals(0, nc.getAccessUids().size());
-        nc.setAccessUids(new ArraySet<>());
-        assertFalse(nc.hasAccessUids());
-        assertFalse(nc.isAccessUid(0));
-        assertFalse(nc.isAccessUid(1000));
-        assertEquals(0, nc.getAccessUids().size());
+        assertThrows(NullPointerException.class, () -> nc.setAllowedUids(null));
+        assertFalse(nc.hasAllowedUids());
+        assertFalse(nc.isUidWithAccess(0));
+        assertFalse(nc.isUidWithAccess(1000));
+        assertEquals(0, nc.getAllowedUids().size());
+        nc.setAllowedUids(new ArraySet<>());
+        assertFalse(nc.hasAllowedUids());
+        assertFalse(nc.isUidWithAccess(0));
+        assertFalse(nc.isUidWithAccess(1000));
+        assertEquals(0, nc.getAllowedUids().size());
 
         final ArraySet<Integer> uids = new ArraySet<>();
         uids.add(200);
         uids.add(250);
         uids.add(-1);
         uids.add(Integer.MAX_VALUE);
-        nc.setAccessUids(uids);
+        nc.setAllowedUids(uids);
         assertNotEquals(nc, new NetworkCapabilities());
-        assertTrue(nc.hasAccessUids());
+        assertTrue(nc.hasAllowedUids());
 
         final List<Integer> includedList = List.of(-2, 0, 199, 700, 901, 1000, Integer.MIN_VALUE);
         final List<Integer> excludedList = List.of(-1, 200, 250, Integer.MAX_VALUE);
         for (final int uid : includedList) {
-            assertFalse(nc.isAccessUid(uid));
+            assertFalse(nc.isUidWithAccess(uid));
         }
         for (final int uid : excludedList) {
-            assertTrue(nc.isAccessUid(uid));
+            assertTrue(nc.isUidWithAccess(uid));
         }
 
-        final Set<Integer> outUids = nc.getAccessUids();
+        final Set<Integer> outUids = nc.getAllowedUids();
         assertEquals(4, outUids.size());
         for (final int uid : includedList) {
             assertFalse(outUids.contains(uid));
@@ -361,10 +364,10 @@
             .addCapability(NET_CAPABILITY_EIMS)
             .addCapability(NET_CAPABILITY_NOT_METERED);
         if (isAtLeastS()) {
-            final ArraySet<Integer> accessUids = new ArraySet<>();
-            accessUids.add(4);
-            accessUids.add(9);
-            netCap.setAccessUids(accessUids);
+            final ArraySet<Integer> allowedUids = new ArraySet<>();
+            allowedUids.add(4);
+            allowedUids.add(9);
+            netCap.setAllowedUids(allowedUids);
             netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
             netCap.setUids(uids);
         }
@@ -489,7 +492,7 @@
         assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testOemPrivate() {
         NetworkCapabilities nc = new NetworkCapabilities();
         // By default OEM_PRIVATE is neither in the required or forbidden lists and the network is
@@ -516,7 +519,7 @@
         assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testForbiddenCapabilities() {
         NetworkCapabilities network = new NetworkCapabilities();
 
@@ -630,7 +633,7 @@
         return new Range<Integer>(from, to);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testSetAdministratorUids() {
         NetworkCapabilities nc =
                 new NetworkCapabilities().setAdministratorUids(new int[] {2, 1, 3});
@@ -638,7 +641,7 @@
         assertArrayEquals(new int[] {1, 2, 3}, nc.getAdministratorUids());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testSetAdministratorUidsWithDuplicates() {
         try {
             new NetworkCapabilities().setAdministratorUids(new int[] {1, 1});
@@ -750,7 +753,7 @@
                 () -> nc2.addTransportType(TRANSPORT_WIFI));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // New behavior in updatable NetworkCapabilities (S+)
+    @Test
     public void testSetNetworkSpecifierOnTestMultiTransportNc() {
         final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
         NetworkCapabilities nc = new NetworkCapabilities.Builder()
@@ -859,7 +862,7 @@
         assertEquals(TRANSPORT_TEST, transportTypes[3]);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testTelephonyNetworkSpecifier() {
         final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
         final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
@@ -970,7 +973,7 @@
         assertEquals(specifier, nc.getNetworkSpecifier());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testAdministratorUidsAndOwnerUid() {
         // Test default owner uid.
         // If the owner uid is not set, the default value should be Process.INVALID_UID.
@@ -1014,7 +1017,7 @@
         return nc;
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testSubIds() throws Exception {
         final NetworkCapabilities ncWithoutId = capsWithSubIds();
         final NetworkCapabilities ncWithId = capsWithSubIds(TEST_SUBID1);
@@ -1036,7 +1039,7 @@
         assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithId));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testEqualsSubIds() throws Exception {
         assertEquals(capsWithSubIds(), capsWithSubIds());
         assertNotEquals(capsWithSubIds(), capsWithSubIds(TEST_SUBID1));
@@ -1185,7 +1188,7 @@
         }
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testBuilder() {
         final int ownerUid = 1001;
         final int signalStrength = -80;
@@ -1255,7 +1258,7 @@
         }
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testBuilderWithoutDefaultCap() {
         final NetworkCapabilities nc =
                 NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
@@ -1266,12 +1269,12 @@
         assertEquals(0, nc.getCapabilities().length);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByNotOwnerWithNonRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithNonRestrictedNc(false /* isOwner */);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByOwnerWithNonRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithNonRestrictedNc(true /* isOwner */);
     }
@@ -1316,12 +1319,12 @@
         assertEquals(expectedNcBuilder.build(), nonRestrictedNc);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByNotOwnerWithRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithRestrictedNc(false /* isOwner */);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByOwnerWithRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithRestrictedNc(true /* isOwner */);
     }
diff --git a/tests/common/tethering-jni-jarjar-rules.txt b/tests/common/tethering-jni-jarjar-rules.txt
new file mode 100644
index 0000000..593ba14
--- /dev/null
+++ b/tests/common/tethering-jni-jarjar-rules.txt
@@ -0,0 +1,10 @@
+# Match the tethering jarjar rules for utils backed by
+# libcom_android_networkstack_tethering_util_jni, so that this JNI library can be used as-is in the
+# test. The alternative would be to build a test-specific JNI library
+# (libcom_android_connectivity_tests_coverage_jni ?) that registers classes following whatever
+# jarjar rules the test is using, but this is a bit less realistic (using a different JNI library),
+# and complicates the test build. It would be necessary if TetheringUtils had a different package
+# name in test code though, as the JNI library name is deducted from the TetheringUtils package.
+rule com.android.net.module.util.BpfMap* com.android.networkstack.tethering.util.BpfMap@1
+rule com.android.net.module.util.BpfUtils* com.android.networkstack.tethering.util.BpfUtils@1
+rule com.android.net.module.util.TcUtils* com.android.networkstack.tethering.util.TcUtils@1
diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS
index 8dfa455..875b4a2 100644
--- a/tests/cts/OWNERS
+++ b/tests/cts/OWNERS
@@ -1,3 +1,3 @@
-# Bug component: 31808
+# Bug template url: http://b/new?component=31808
 set noparent
 file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking_xts
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index f0d7133..3c4c3e3 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -149,7 +149,7 @@
 
     private static final String APP_NOT_FOREGROUND_ERROR = "app_not_fg";
 
-    protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 5_000; // 5 sec
+    protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
 
     private static final long BROADCAST_TIMEOUT_MS = 5_000;
 
@@ -329,7 +329,8 @@
                     + "; sleeping 1s before trying again");
             SystemClock.sleep(SECOND_IN_MS);
         }
-        fail("App2 is not on background state after " + maxTries + " attempts: " + state );
+        fail("App2 (" + mUid + ") is not on background state after "
+                + maxTries + " attempts: " + state);
     }
 
     protected final void assertForegroundState() throws Exception {
@@ -347,7 +348,8 @@
             turnScreenOn();
             SystemClock.sleep(SECOND_IN_MS);
         }
-        fail("App2 is not on foreground state after " + maxTries + " attempts: " + state );
+        fail("App2 (" + mUid + ") is not on foreground state after "
+                + maxTries + " attempts: " + state);
     }
 
     protected final void assertForegroundServiceState() throws Exception {
@@ -364,7 +366,8 @@
                     + "; sleeping 1s before trying again");
             SystemClock.sleep(SECOND_IN_MS);
         }
-        fail("App2 is not on foreground service state after " + maxTries + " attempts: " + state );
+        fail("App2 (" + mUid + ") is not on foreground service state after "
+                + maxTries + " attempts: " + state);
     }
 
     /**
@@ -406,8 +409,8 @@
             // Exponential back-off.
             timeoutMs = Math.min(timeoutMs*2, NETWORK_TIMEOUT_MS);
         }
-        fail("Invalid state for expectAvailable=" + expectAvailable + " after " + maxTries
-                + " attempts.\nLast error: " + error);
+        fail("Invalid state for " + mUid + "; expectAvailable=" + expectAvailable + " after "
+                + maxTries + " attempts.\nLast error: " + error);
     }
 
     /**
@@ -763,7 +766,7 @@
             Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
             SystemClock.sleep(SECOND_IN_MS);
         }
-        fail("app2 receiver is not ready");
+        fail("app2 receiver is not ready in " + mUid);
     }
 
     protected void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
@@ -878,6 +881,11 @@
         }
     }
 
+    protected void startActivity() throws Exception {
+        final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
+        mContext.startActivity(launchIntent);
+    }
+
     private void startForegroundService() throws Exception {
         final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_FOREGROUND_SERVICE);
         mContext.startForegroundService(launchIntent);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
index 604a0b6..2f30536 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
@@ -20,6 +20,7 @@
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
 
+import static com.android.compatibility.common.util.FeatureUtil.isTV;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
 import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
 import static com.android.cts.net.hostside.Property.METERED_NETWORK;
@@ -27,14 +28,14 @@
 
 import static org.junit.Assert.fail;
 
+import androidx.test.filters.LargeTest;
+
 import com.android.compatibility.common.util.CddTest;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import androidx.test.filters.LargeTest;
-
 @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
 @LargeTest
 public class DataSaverModeTest extends AbstractRestrictBackgroundNetworkTestCase {
@@ -113,6 +114,11 @@
         turnScreenOff();
         assertBackgroundNetworkAccess(false);
         turnScreenOn();
+        // On some TVs, it is possible that the activity on top may change after the screen is
+        // turned off and on again, so relaunch the activity in the test app again.
+        if (isTV()) {
+            startActivity();
+        }
         assertForegroundNetworkAccess();
 
         // Goes back to background state.
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
index cb0341c..78ae7b8 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
@@ -62,6 +62,8 @@
                     "dumpsys network_management",
                     "dumpsys usagestats " + TEST_PKG + " " + TEST_APP2_PKG,
                     "dumpsys usagestats appstandby",
+                    "dumpsys connectivity trafficcontroller",
+                    "dumpsys netd trafficcontroller",
             }) {
                 dumpCommandOutput(out, cmd);
             }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index 89a9bd6..56be3e3 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -25,7 +25,7 @@
 import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
 import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
 
-import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
 import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
 
 import static org.junit.Assert.assertEquals;
@@ -38,6 +38,7 @@
 import android.app.Instrumentation;
 import android.app.UiAutomation;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.location.LocationManager;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
@@ -99,6 +100,10 @@
         return mBatterySaverSupported;
     }
 
+    private static boolean isWear() {
+        return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+    }
+
     /**
      * As per CDD requirements, if the device doesn't support data saver mode then
      * ConnectivityManager.getRestrictBackgroundStatus() will always return
@@ -107,6 +112,9 @@
      * RESTRICT_BACKGROUND_STATUS_DISABLED or not.
      */
     public static boolean isDataSaverSupported() {
+        if (isWear()) {
+            return false;
+        }
         if (mDataSaverSupported == null) {
             assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
             try {
@@ -382,7 +390,7 @@
     }
 
     public static String executeShellCommand(String command) {
-        final String result = runShellCommand(command).trim();
+        final String result = runShellCommandOrThrow(command).trim();
         Log.d(TAG, "Output of '" + command + "': '" + result + "'");
         return result;
     }
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 5778b0d..dc67c70 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
@@ -334,9 +334,8 @@
             @Nullable ProxyInfo proxyInfo,
             @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
             throws Exception {
-        startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
-                disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered,
-                false /* addRoutesByIpPrefix */);
+        startVpn(addresses, routes, excludedRoutes, allowedApplications, disallowedApplications,
+                proxyInfo, underlyingNetworks, isAlwaysMetered, false /* addRoutesByIpPrefix */);
     }
 
     private void startVpn(
@@ -638,8 +637,8 @@
 
         if (address instanceof Inet6Address) {
             checkUdpEcho(destination, "2001:db8:1:2::ffe");
-            checkTcpReflection(destination, "2001:db8:1:2::ffe");
             checkPing(destination);
+            checkTcpReflection(destination, "2001:db8:1:2::ffe");
         } else {
             checkUdpEcho(destination, "192.0.2.2");
             checkTcpReflection(destination, "192.0.2.2");
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index d605799..48a1c79 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -30,6 +30,8 @@
     </target_preparer>
     <target_preparer class="com.android.testutils.ConnectivityCheckTargetPreparer">
     </target_preparer>
+    <target_preparer class="com.android.testutils.DisableConfigSyncTargetPreparer">
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.net.cts" />
         <option name="runtime-hint" value="9m4s" />
diff --git a/tests/cts/net/OWNERS b/tests/cts/net/OWNERS
deleted file mode 100644
index df5569e..0000000
--- a/tests/cts/net/OWNERS
+++ /dev/null
@@ -1,5 +0,0 @@
-# Bug component: 31808
-# Inherits parent owners
-per-file src/android/net/cts/NetworkWatchlistTest.java=alanstokes@google.com
-
-# Bug component: 685852 = per-file *IpSec*
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index a378aa7..0344604 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -77,7 +77,7 @@
 
 // Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
 private const val WIFI_CONNECT_TIMEOUT_MS = 40_000L
-private const val TEST_TIMEOUT_MS = 10_000L
+private const val TEST_TIMEOUT_MS = 20_000L
 
 private const val TAG = "CaptivePortalTest"
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index ea64252..a129108 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -151,6 +151,7 @@
 import android.os.Looper;
 import android.os.MessageQueue;
 import android.os.Process;
+import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
@@ -590,8 +591,9 @@
     }
 
     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @AppModeFull(reason = "Cannot get installed packages in instant app mode")
     @Test
-    public void testRedactLinkPropertiesForPackage() throws Exception {
+    public void testGetRedactedLinkPropertiesForPackage() throws Exception {
         final String groundedPkg = findPackageByPermissions(
                 List.of(), /* requiredPermissions */
                 List.of(ACCESS_NETWORK_STATE) /* forbiddenPermissions */);
@@ -628,54 +630,58 @@
         // No matter what the given uid is, a SecurityException will be thrown if the caller
         // doesn't hold the NETWORK_SETTINGS permission.
         assertThrows(SecurityException.class,
-                () -> mCm.redactLinkPropertiesForPackage(lp, groundedUid, groundedPkg));
+                () -> mCm.getRedactedLinkPropertiesForPackage(lp, groundedUid, groundedPkg));
         assertThrows(SecurityException.class,
-                () -> mCm.redactLinkPropertiesForPackage(lp, normalUid, normalPkg));
+                () -> mCm.getRedactedLinkPropertiesForPackage(lp, normalUid, normalPkg));
         assertThrows(SecurityException.class,
-                () -> mCm.redactLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg));
+                () -> mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg));
 
         runAsShell(NETWORK_SETTINGS, () -> {
             // No matter what the given uid is, if the given LinkProperties is null, then
             // NullPointerException will be thrown.
             assertThrows(NullPointerException.class,
-                    () -> mCm.redactLinkPropertiesForPackage(null, groundedUid, groundedPkg));
+                    () -> mCm.getRedactedLinkPropertiesForPackage(null, groundedUid, groundedPkg));
             assertThrows(NullPointerException.class,
-                    () -> mCm.redactLinkPropertiesForPackage(null, normalUid, normalPkg));
+                    () -> mCm.getRedactedLinkPropertiesForPackage(null, normalUid, normalPkg));
             assertThrows(NullPointerException.class,
-                    () -> mCm.redactLinkPropertiesForPackage(null, privilegedUid, privilegedPkg));
+                    () -> mCm.getRedactedLinkPropertiesForPackage(
+                            null, privilegedUid, privilegedPkg));
 
             // Make sure null is returned for a UID without ACCESS_NETWORK_STATE.
-            assertNull(mCm.redactLinkPropertiesForPackage(lp, groundedUid, groundedPkg));
+            assertNull(mCm.getRedactedLinkPropertiesForPackage(lp, groundedUid, groundedPkg));
 
             // CaptivePortalApiUrl & CaptivePortalData will be set to null if given uid doesn't hold
             // the NETWORK_SETTINGS permission.
-            assertNull(mCm.redactLinkPropertiesForPackage(lp, normalUid, normalPkg)
+            assertNull(mCm.getRedactedLinkPropertiesForPackage(lp, normalUid, normalPkg)
                     .getCaptivePortalApiUrl());
-            assertNull(mCm.redactLinkPropertiesForPackage(lp, normalUid, normalPkg)
+            assertNull(mCm.getRedactedLinkPropertiesForPackage(lp, normalUid, normalPkg)
                     .getCaptivePortalData());
             // MTU is not sensitive and is not redacted.
-            assertEquals(mtu, mCm.redactLinkPropertiesForPackage(lp, normalUid, normalPkg)
+            assertEquals(mtu, mCm.getRedactedLinkPropertiesForPackage(lp, normalUid, normalPkg)
                     .getMtu());
 
             // CaptivePortalApiUrl & CaptivePortalData will be preserved if the given uid holds the
             // NETWORK_SETTINGS permission.
-            assertEquals(capportUrl,
-                    mCm.redactLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
+            assertNotNull(lp.getCaptivePortalApiUrl());
+            assertNotNull(lp.getCaptivePortalData());
+            assertEquals(lp.getCaptivePortalApiUrl(),
+                    mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                             .getCaptivePortalApiUrl());
-            assertEquals(capportData,
-                    mCm.redactLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
+            assertEquals(lp.getCaptivePortalData(),
+                    mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                             .getCaptivePortalData());
         });
     }
 
     private NetworkCapabilities redactNc(@NonNull final NetworkCapabilities nc, int uid,
             @NonNull String packageName) {
-        return mCm.redactNetworkCapabilitiesForPackage(nc, uid, packageName);
+        return mCm.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName);
     }
 
     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @AppModeFull(reason = "Cannot get installed packages in instant app mode")
     @Test
-    public void testRedactNetworkCapabilitiesForPackage() throws Exception {
+    public void testGetRedactedNetworkCapabilitiesForPackage() throws Exception {
         final String groundedPkg = findPackageByPermissions(
                 List.of(), /* requiredPermissions */
                 List.of(ACCESS_NETWORK_STATE) /* forbiddenPermissions */);
@@ -884,9 +890,21 @@
         //
         // Note that this test this will still fail in instant mode if a device supports Ethernet
         // via other hardware means. We are not currently aware of any such device.
-        return (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) ||
-            mPackageManager.hasSystemFeature(FEATURE_ETHERNET) ||
-            mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+        return hasEthernetService()
+                || mPackageManager.hasSystemFeature(FEATURE_ETHERNET)
+                || mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+    }
+
+    private boolean hasEthernetService() {
+        // On Q creating EthernetManager from a thread that does not have a looper (like the test
+        // thread) crashes because it tried to use Looper.myLooper() through the default Handler
+        // constructor to run onAvailabilityChanged callbacks. Use ServiceManager to check whether
+        // the service exists instead.
+        // TODO: remove once Q is no longer supported in MTS, as ServiceManager is hidden API
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+            return ServiceManager.getService(Context.ETHERNET_SERVICE) != null;
+        }
+        return mContext.getSystemService(Context.ETHERNET_SERVICE) != null;
     }
 
     private boolean shouldBeSupported(int networkType) {
@@ -2174,7 +2192,7 @@
     private void waitForAvailable(
             @NonNull final TestableNetworkCallback cb, @NonNull final Network expectedNetwork) {
         cb.expectAvailableCallbacks(expectedNetwork, false /* suspended */,
-                true /* validated */,
+                null /* validated */,
                 false /* blocked */, NETWORK_CALLBACK_TIMEOUT_MS);
     }
 
@@ -2908,9 +2926,8 @@
             // Default network should be updated to validated cellular network.
             defaultCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
                     entry -> cellNetwork.equals(entry.getNetwork()));
-            // No callback except LinkPropertiesChanged which may be triggered randomly from network
-            wifiCb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS,
-                    c -> !(c instanceof CallbackEntry.LinkPropertiesChanged));
+            // The network should not validate again.
+            wifiCb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS, c -> isValidatedCaps(c));
         } finally {
             resetAvoidBadWifi(previousAvoidBadWifi);
             resetValidationConfig();
@@ -2919,6 +2936,12 @@
         }
     }
 
+    private boolean isValidatedCaps(CallbackEntry c) {
+        if (!(c instanceof CallbackEntry.CapabilitiesChanged)) return false;
+        final CallbackEntry.CapabilitiesChanged capsChanged = (CallbackEntry.CapabilitiesChanged) c;
+        return capsChanged.getCaps().hasCapability(NET_CAPABILITY_VALIDATED);
+    }
+
     private void resetAvoidBadWifi(int settingValue) {
         setTestAllowBadWifiResource(0 /* timeMs */);
         ConnectivitySettingsManager.setNetworkAvoidBadWifi(mContext, settingValue);
@@ -3259,6 +3282,19 @@
         assertTrue(dumpOutput, dumpOutput.contains("Active default network"));
     }
 
+    @Test @IgnoreUpTo(SC_V2)
+    public void testDumpBpfNetMaps() throws Exception {
+        final String[] args = new String[] {"--short", "trafficcontroller"};
+        String dumpOutput = DumpTestUtils.dumpServiceWithShellPermission(
+                Context.CONNECTIVITY_SERVICE, args);
+        assertTrue(dumpOutput, dumpOutput.contains("TrafficController"));
+        assertFalse(dumpOutput, dumpOutput.contains("BPF map content"));
+
+        dumpOutput = DumpTestUtils.dumpServiceWithShellPermission(
+                Context.CONNECTIVITY_SERVICE, args[1]);
+        assertTrue(dumpOutput, dumpOutput.contains("BPF map content"));
+    }
+
     private void unregisterRegisteredCallbacks() {
         for (NetworkCallback callback: mRegisteredCallbacks) {
             mCm.unregisterNetworkCallback(callback);
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index 886b078..1e42fe6 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -22,15 +22,14 @@
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.content.Context
 import android.net.ConnectivityManager
-import android.net.cts.util.CtsNetUtils
 import android.net.DscpPolicy
-import android.net.DscpPolicy.STATUS_DELETED
-import android.net.DscpPolicy.STATUS_SUCCESS
 import android.net.InetAddresses
 import android.net.IpPrefix
 import android.net.LinkAddress
 import android.net.LinkProperties
 import android.net.NetworkAgent
+import android.net.NetworkAgent.DSCP_POLICY_STATUS_DELETED
+import android.net.NetworkAgent.DSCP_POLICY_STATUS_SUCCESS
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
@@ -45,15 +44,11 @@
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
 import android.net.RouteInfo
-import android.net.util.SocketUtils
-import android.os.Build
 import android.os.HandlerThread
-import android.os.Looper
 import android.platform.test.annotations.AppModeFull
 import android.system.Os
-import android.system.OsConstants
 import android.system.OsConstants.AF_INET
-import android.system.OsConstants.IPPROTO_IP
+import android.system.OsConstants.AF_INET6
 import android.system.OsConstants.IPPROTO_UDP
 import android.system.OsConstants.SOCK_DGRAM
 import android.system.OsConstants.SOCK_NONBLOCK
@@ -61,10 +56,8 @@
 import android.util.Range
 import androidx.test.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
-import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.CompatUtil
 import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.OffsetFilter
 import com.android.testutils.assertParcelingIsLossless
 import com.android.testutils.runAsShell
 import com.android.testutils.SC_V2
@@ -74,28 +67,27 @@
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated
 import com.android.testutils.TestableNetworkCallback
 import org.junit.After
-import org.junit.AfterClass
+import org.junit.Assume.assumeTrue
 import org.junit.Before
-import org.junit.BeforeClass
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.net.Inet4Address
 import java.net.Inet6Address
 import java.net.InetAddress
-import java.net.InetSocketAddress
-import java.net.ServerSocket
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
-import java.util.UUID
+import java.util.regex.Pattern
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
-import kotlin.concurrent.thread
 import kotlin.test.fail
 
 private const val MAX_PACKET_LENGTH = 1500
 
+private const val IP4_PREFIX_LEN = 32
+private const val IP6_PREFIX_LEN = 128
+
 private val instrumentation: Instrumentation
     get() = InstrumentationRegistry.getInstrumentation()
 
@@ -112,6 +104,9 @@
     private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
     private val TEST_TARGET_IPV4_ADDR =
             InetAddresses.parseNumericAddress("8.8.8.8") as Inet4Address
+    private val LOCAL_IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::1")
+    private val TEST_TARGET_IPV6_ADDR =
+            InetAddresses.parseNumericAddress("2001:4860:4860::8888") as Inet6Address
 
     private val realContext = InstrumentationRegistry.getContext()
     private val cm = realContext.getSystemService(ConnectivityManager::class.java)
@@ -125,12 +120,31 @@
     private lateinit var tunNetworkCallback: TestNetworkCallback
     private lateinit var reader: TapPacketReader
 
+    private fun getKernelVersion(): IntArray {
+        // Example:
+        // 4.9.29-g958411d --> 4.9
+        val release = Os.uname().release
+        val m = Pattern.compile("^(\\d+)\\.(\\d+)").matcher(release)
+        assertTrue(m.find(), "No pattern in release string: " + release)
+        return intArrayOf(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)))
+    }
+
+    private fun kernelIsAtLeast(major: Int, minor: Int): Boolean {
+        val version = getKernelVersion()
+        return (version.get(0) > major || (version.get(0) == major && version.get(1) >= minor))
+    }
+
     @Before
     fun setUp() {
+        // For BPF support kernel needs to be at least 5.4.
+        assumeTrue(kernelIsAtLeast(5, 4))
+
         runAsShell(MANAGE_TEST_NETWORKS) {
             val tnm = realContext.getSystemService(TestNetworkManager::class.java)
 
-            iface = tnm.createTunInterface( Array(1){ LinkAddress(LOCAL_IPV4_ADDRESS, 32) } )
+            iface = tnm.createTunInterface(arrayOf(
+                    LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN),
+                    LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN)))
             assertNotNull(iface)
         }
 
@@ -144,11 +158,16 @@
 
     @After
     fun tearDown() {
+        if (!kernelIsAtLeast(5, 4)) {
+            return;
+        }
         agentsToCleanUp.forEach { it.unregister() }
         callbacksToCleanUp.forEach { cm.unregisterNetworkCallback(it) }
 
         // reader.stop() cleans up tun fd
         reader.handler.post { reader.stop() }
+        if (iface.fileDescriptor.fileDescriptor != null)
+            Os.close(iface.fileDescriptor.fileDescriptor)
         handlerThread.quitSafely()
     }
 
@@ -172,7 +191,7 @@
 
     private fun createConnectedNetworkAgent(
         context: Context = realContext,
-        specifier: String? = iface.getInterfaceName(),
+        specifier: String? = iface.getInterfaceName()
     ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
         val callback = TestableNetworkCallback()
         // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
@@ -191,9 +210,11 @@
             }
         }
         val lp = LinkProperties().apply {
-            addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+            addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN))
+            addLinkAddress(LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN))
             addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
-            setInterfaceName(iface.getInterfaceName())
+            addRoute(RouteInfo(InetAddress.getByName("fe80::1234")))
+            setInterfaceName(specifier)
         }
         val config = NetworkAgentConfig.Builder().build()
         val agent = TestableNetworkAgent(context, handlerThread.looper, nc, lp, config)
@@ -213,47 +234,114 @@
         eachByte -> "%02x".format(eachByte)
     }
 
-    fun checkDscpValue(
-        agent : TestableNetworkAgent,
-        callback : TestableNetworkCallback,
-        dscpValue : Int = 0,
-        dstPort : Int = 0,
+    fun sendPacket(
+        agent: TestableNetworkAgent,
+        sendV6: Boolean,
+        dstPort: Int = 0,
     ) {
         val testString = "test string"
         val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
         var packetFound = false
 
-        val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP)
+        val socket = Os.socket(if (sendV6) AF_INET6 else AF_INET, SOCK_DGRAM or SOCK_NONBLOCK,
+                IPPROTO_UDP)
         agent.network.bindSocket(socket)
 
         val originalPacket = testPacket.readAsArray()
-        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size,
-                0 /* flags */, TEST_TARGET_IPV4_ADDR, dstPort)
-
+        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
+                if(sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
         Os.close(socket)
+    }
+
+    fun parseV4PacketDscp(buffer : ByteBuffer) : Int {
+        val ip_ver = buffer.get()
+        val tos = buffer.get()
+        val length = buffer.getShort()
+        val id = buffer.getShort()
+        val offset = buffer.getShort()
+        val ttl = buffer.get()
+        val ipType = buffer.get()
+        val checksum = buffer.getShort()
+        return tos.toInt().shr(2)
+    }
+
+    fun parseV6PacketDscp(buffer : ByteBuffer) : Int {
+        val ip_ver = buffer.get()
+        val tc = buffer.get()
+        val fl = buffer.getShort()
+        val length = buffer.getShort()
+        val proto = buffer.get()
+        val hop = buffer.get()
+        // DSCP is bottom 4 bits of ip_ver and top 2 of tc.
+        val ip_ver_bottom = ip_ver.toInt().and(0xf)
+        val tc_dscp = tc.toInt().shr(6)
+        return ip_ver_bottom.toInt().shl(2) + tc_dscp
+    }
+
+    fun parsePacketIp(
+        buffer : ByteBuffer,
+        sendV6 : Boolean,
+    ) : Boolean {
+        val ipAddr = if (sendV6) ByteArray(16) else ByteArray(4)
+        buffer.get(ipAddr)
+        val srcIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
+                else Inet4Address.getByAddress(ipAddr)
+        buffer.get(ipAddr)
+        val dstIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
+                else Inet4Address.getByAddress(ipAddr)
+
+        Log.e(TAG, "IP Src:" + srcIp + " dst: " + dstIp)
+
+        if ((sendV6 && srcIp == LOCAL_IPV6_ADDRESS && dstIp == TEST_TARGET_IPV6_ADDR) ||
+                (!sendV6 && srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR)) {
+            Log.e(TAG, "IP return true");
+            return true
+        }
+        Log.e(TAG, "IP return false");
+        return false
+    }
+
+    fun parsePacketPort(
+        buffer : ByteBuffer,
+        srcPort : Int,
+        dstPort : Int
+    ) : Boolean {
+        if (srcPort == 0 && dstPort == 0) return true
+
+        val packetSrcPort = buffer.getShort().toInt()
+        val packetDstPort = buffer.getShort().toInt()
+
+        Log.e(TAG, "Port Src:" + packetSrcPort + " dst: " + packetDstPort)
+
+        if ((srcPort == 0 || (srcPort != 0 && srcPort == packetSrcPort)) &&
+                (dstPort == 0 || (dstPort != 0 && dstPort == packetDstPort))) {
+            Log.e(TAG, "Port return true");
+            return true
+        }
+        Log.e(TAG, "Port return false");
+        return false
+    }
+
+    fun validatePacket(
+        agent : TestableNetworkAgent,
+        sendV6 : Boolean = false,
+        dscpValue : Int = 0,
+        dstPort : Int = 0,
+    ) {
+        var packetFound = false;
+        sendPacket(agent, sendV6, dstPort)
+        // TODO: grab source port from socket in sendPacket
+
+        Log.e(TAG, "find DSCP value:" + dscpValue)
         generateSequence { reader.poll(PACKET_TIMEOUT_MS) }.forEach { packet ->
             val buffer = ByteBuffer.wrap(packet, 0, packet.size).order(ByteOrder.BIG_ENDIAN)
-            val ip_ver = buffer.get()
-            val tos = buffer.get()
-            val length = buffer.getShort()
-            val id = buffer.getShort()
-            val offset = buffer.getShort()
-            val ttl = buffer.get()
-            val ipType = buffer.get()
-            val checksum = buffer.getShort()
+            val dscp = if (sendV6) parseV6PacketDscp(buffer) else parseV4PacketDscp(buffer)
+            Log.e(TAG, "DSCP value:" + dscp)
 
-            val ipAddr = ByteArray(4)
-            buffer.get(ipAddr)
-            val srcIp = Inet4Address.getByAddress(ipAddr);
-            buffer.get(ipAddr)
-            val dstIp = Inet4Address.getByAddress(ipAddr);
-            val packetSrcPort = buffer.getShort().toInt()
-            val packetDstPort = buffer.getShort().toInt()
-
-            // TODO: Add source port comparison.
-            if (srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR &&
-                    packetDstPort == dstPort) {
-                assertEquals(dscpValue, (tos.toInt().shr(2)))
+            // TODO: Add source port comparison. Use 0 for now.
+            if (parsePacketIp(buffer, sendV6) && parsePacketPort(buffer, 0, dstPort)) {
+                Log.e(TAG, "DSCP value found")
+                assertEquals(dscpValue, dscp)
                 packetFound = true
             }
         }
@@ -261,52 +349,90 @@
     }
 
     fun doRemovePolicyTest(
-        agent : TestableNetworkAgent,
-        callback : TestableNetworkCallback,
-        policyId : Int
+        agent: TestableNetworkAgent,
+        callback: TestableNetworkCallback,
+        policyId: Int
     ) {
         val portNumber = 1111 * policyId
         agent.sendRemoveDscpPolicy(policyId)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(policyId, it.policyId)
-            assertEquals(STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = portNumber)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
         }
     }
 
     @Test
-    fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+    fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
         val policy = DscpPolicy.Builder(1, 1)
                 .setDestinationPortRange(Range(4444, 4444)).build()
         agent.sendAddDscpPolicy(policy)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
-
-        checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+        validatePacket(agent, dscpValue = 1, dstPort = 4444)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_DELETED, it.status)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
         }
 
         val policy2 = DscpPolicy.Builder(1, 4)
-                .setDestinationPortRange(Range(5555, 5555)).setSourceAddress(LOCAL_IPV4_ADDRESS)
-                .setDestinationAddress(TEST_TARGET_IPV4_ADDR).setProtocol(IPPROTO_UDP).build()
+                .setDestinationPortRange(Range(5555, 5555))
+                .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+                .setSourceAddress(LOCAL_IPV4_ADDRESS)
+                .setProtocol(IPPROTO_UDP).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
 
-        checkDscpValue(agent, callback, dscpValue = 4, dstPort = 5555)
+        validatePacket(agent, dscpValue = 4, dstPort = 5555)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_DELETED, it.status)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+        }
+    }
+
+    @Test
+    fun testDscpPolicyAddV6Policies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1)
+                .setDestinationPortRange(Range(4444, 4444)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+        }
+        validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+        }
+
+        val policy2 = DscpPolicy.Builder(1, 4)
+                .setDestinationPortRange(Range(5555, 5555))
+                .setDestinationAddress(TEST_TARGET_IPV6_ADDR)
+                .setSourceAddress(LOCAL_IPV6_ADDRESS)
+                .setProtocol(IPPROTO_UDP).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+        }
+        validatePacket(agent, true, dscpValue = 4, dstPort = 5555)
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
         }
     }
 
@@ -318,41 +444,44 @@
         agent.sendAddDscpPolicy(policy)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
         agent.sendAddDscpPolicy(policy3)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         /* Remove Policies and check CE is no longer set */
         doRemovePolicyTest(agent, callback, 1)
+        validatePacket(agent, dscpValue = 0, dstPort = 1111)
         doRemovePolicyTest(agent, callback, 2)
+        validatePacket(agent, dscpValue = 0, dstPort = 2222)
         doRemovePolicyTest(agent, callback, 3)
+        validatePacket(agent, dscpValue = 0, dstPort = 3333)
     }
 
     @Test
     fun testRemoveDscpPolicy_RemoveImmediatelyAfterAdd(): Unit =
-            createConnectedNetworkAgent().let{ (agent, callback) ->
+            createConnectedNetworkAgent().let { (agent, callback) ->
         val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build()
         agent.sendAddDscpPolicy(policy)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
         doRemovePolicyTest(agent, callback, 1)
 
@@ -360,8 +489,8 @@
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
         doRemovePolicyTest(agent, callback, 2)
 
@@ -369,8 +498,8 @@
         agent.sendAddDscpPolicy(policy3)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
         doRemovePolicyTest(agent, callback, 3)
     }
@@ -383,24 +512,24 @@
         agent.sendAddDscpPolicy(policy)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
         agent.sendAddDscpPolicy(policy3)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         /* Remove Policies and check CE is no longer set */
@@ -414,18 +543,19 @@
                 (agent, callback) ->
         agent.sendRemoveDscpPolicy(3)
         // Is there something to add in TestableNetworkCallback to NOT expect a callback?
-        // Or should we send STATUS_DELETED in any case or a different STATUS?
+        // Or should we send DSCP_POLICY_STATUS_DELETED in any case or a different STATUS?
     }
 
     @Test
-    fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+    fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
         val policy = DscpPolicy.Builder(1, 1)
                 .setDestinationPortRange(Range(1111, 1111)).build()
         agent.sendAddDscpPolicy(policy)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1)
@@ -433,8 +563,8 @@
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1)
@@ -442,25 +572,25 @@
         agent.sendAddDscpPolicy(policy3)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         agent.sendRemoveAllDscpPolicies()
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 1111)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+            validatePacket(agent, false, dstPort = 1111)
         }
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
-            assertEquals(STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 2222)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+            validatePacket(agent, false, dstPort = 2222)
         }
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
-            assertEquals(STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 3333)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+            validatePacket(agent, false, dstPort = 3333)
         }
     }
 
@@ -471,28 +601,25 @@
         agent.sendAddDscpPolicy(policy)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+            validatePacket(agent, dscpValue = 1, dstPort = 4444)
         }
 
-        // TODO: send packet on socket and confirm that changing the DSCP policy
-        // updates the mark to the new value.
-
         val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_SUCCESS, it.status)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
 
             // Sending packet with old policy should fail
-            checkDscpValue(agent, callback, dstPort = 4444)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 5555)
+            validatePacket(agent, dscpValue = 0, dstPort = 4444)
+            validatePacket(agent, dscpValue = 1, dstPort = 5555)
         }
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
-            assertEquals(STATUS_DELETED, it.status)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
         }
     }
 
@@ -501,14 +628,14 @@
                 (agent, callback) ->
         // Check that policy with partial parameters is lossless.
         val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
-        assertParcelingIsLossless(policy);
+        assertParcelingIsLossless(policy)
 
         // Check that policy with all parameters is lossless.
         val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444))
                 .setSourceAddress(LOCAL_IPV4_ADDRESS)
                 .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
                 .setProtocol(IPPROTO_UDP).build()
-        assertParcelingIsLossless(policy2);
+        assertParcelingIsLossless(policy2)
     }
 }
 
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
new file mode 100644
index 0000000..30e0015
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.net.InetAddresses
+import android.net.IpConfiguration
+import android.net.MacAddress
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.platform.test.annotations.AppModeFull
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.net.module.util.TrackRecord
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import com.android.testutils.runAsShell
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import android.content.Context
+import org.junit.runner.RunWith
+import kotlin.test.assertNull
+import kotlin.test.fail
+import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Handler
+import android.os.HandlerExecutor
+import android.os.Looper
+import com.android.networkstack.apishim.common.EthernetManagerShim.InterfaceStateListener
+import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_ABSENT
+import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_LINK_DOWN
+import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_LINK_UP
+import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_CLIENT
+import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_NONE
+import com.android.networkstack.apishim.EthernetManagerShimImpl
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReader
+import com.android.testutils.waitForIdle
+import java.net.Inet6Address
+import java.util.concurrent.Executor
+import kotlin.test.assertFalse
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import java.net.NetworkInterface
+
+private const val TIMEOUT_MS = 1000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+private val DEFAULT_IP_CONFIGURATION = IpConfiguration(IpConfiguration.IpAssignment.DHCP,
+    IpConfiguration.ProxySettings.NONE, null, null)
+
+@AppModeFull(reason = "Instant apps can't access EthernetManager")
+@RunWith(AndroidJUnit4::class)
+class EthernetManagerTest {
+    // EthernetManager is not updatable before T, so tests do not need to be backwards compatible
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val em by lazy { EthernetManagerShimImpl.newInstance(context) }
+
+    private val createdIfaces = ArrayList<EthernetTestInterface>()
+    private val addedListeners = ArrayList<InterfaceStateListener>()
+
+    private class EthernetTestInterface(
+        context: Context,
+        private val handler: Handler
+    ) {
+        private val tapInterface: TestNetworkInterface
+        private val packetReader: TapPacketReader
+        private val raResponder: RouterAdvertisementResponder
+        val interfaceName get() = tapInterface.interfaceName
+
+        init {
+            tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
+                val tnm = context.getSystemService(TestNetworkManager::class.java)
+                tnm.createTapInterface(false /* bringUp */)
+            }
+            val mtu = NetworkInterface.getByName(tapInterface.interfaceName).getMTU()
+            packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
+            raResponder = RouterAdvertisementResponder(packetReader)
+            raResponder.addRouterEntry(MacAddress.fromString("01:23:45:67:89:ab"),
+                    InetAddresses.parseNumericAddress("fe80::abcd") as Inet6Address)
+
+            packetReader.startAsyncForTest()
+            raResponder.start()
+        }
+
+        fun destroy() {
+            raResponder.stop()
+            handler.post({ packetReader.stop() })
+            handler.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    private open class EthernetStateListener private constructor(
+        private val history: ArrayTrackRecord<CallbackEntry>
+    ) : InterfaceStateListener,
+                TrackRecord<EthernetStateListener.CallbackEntry> by history {
+        constructor() : this(ArrayTrackRecord())
+
+        val events = history.newReadHead()
+
+        sealed class CallbackEntry {
+            data class InterfaceStateChanged(
+                val iface: String,
+                val state: Int,
+                val role: Int,
+                val configuration: IpConfiguration?
+            ) : CallbackEntry()
+        }
+
+        override fun onInterfaceStateChanged(
+            iface: String,
+            state: Int,
+            role: Int,
+            cfg: IpConfiguration?
+        ) {
+            add(InterfaceStateChanged(iface, state, role, cfg))
+        }
+
+        fun <T : CallbackEntry> expectCallback(expected: T): T {
+            val event = pollForNextCallback()
+            assertEquals(expected, event)
+            return event as T
+        }
+
+        fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) {
+            expectCallback(InterfaceStateChanged(iface.interfaceName, state, role,
+                if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null))
+        }
+
+        fun pollForNextCallback(): CallbackEntry {
+            return events.poll(TIMEOUT_MS) ?: fail("Did not receive callback after ${TIMEOUT_MS}ms")
+        }
+
+        fun assertNoCallback() {
+            val cb = events.poll(NO_CALLBACK_TIMEOUT_MS)
+            assertNull(cb, "Expected no callback but got $cb")
+        }
+    }
+
+    @Before
+    fun setUp() {
+        setIncludeTestInterfaces(true)
+    }
+
+    @After
+    fun tearDown() {
+        setIncludeTestInterfaces(false)
+        for (iface in createdIfaces) {
+            iface.destroy()
+        }
+        for (listener in addedListeners) {
+            em.removeInterfaceStateListener(listener)
+        }
+    }
+
+    private fun addInterfaceStateListener(executor: Executor, listener: InterfaceStateListener) {
+        em.addInterfaceStateListener(executor, listener)
+        addedListeners.add(listener)
+    }
+
+    private fun createInterface(): EthernetTestInterface {
+        return EthernetTestInterface(context, Handler(Looper.getMainLooper()))
+    }
+
+    private fun setIncludeTestInterfaces(value: Boolean) {
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(value)
+        }
+    }
+
+    private fun removeInterface(iface: EthernetTestInterface) {
+        iface.destroy()
+        createdIfaces.remove(iface)
+    }
+
+    @Test
+    public fun testCallbacks() {
+        val executor = HandlerExecutor(Handler(Looper.getMainLooper()))
+
+        // If an interface exists when the callback is registered, it is reported on registration.
+        val iface = createInterface()
+        val listener = EthernetStateListener()
+        addInterfaceStateListener(executor, listener)
+        listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+        // If an interface appears, existing callbacks see it.
+        // TODO: fix the up/up/down/up callbacks and only send down/up.
+        val iface2 = createInterface()
+        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+
+        // Removing interfaces first sends link down, then STATE_ABSENT/ROLE_NONE.
+        removeInterface(iface)
+        listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+        listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+
+        removeInterface(iface2)
+        listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+        listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
+        listener.assertNoCallback()
+    }
+
+    @Test
+    public fun testGetInterfaceList() {
+        setIncludeTestInterfaces(true)
+
+        // Create two test interfaces and check the return list contains the interface names.
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+        var ifaces = em.getInterfaceList()
+        assertTrue(ifaces.size > 0)
+        assertTrue(ifaces.contains(iface1.interfaceName))
+        assertTrue(ifaces.contains(iface2.interfaceName))
+
+        // Remove one existing test interface and check the return list doesn't contain the
+        // removed interface name.
+        removeInterface(iface1)
+        ifaces = em.getInterfaceList()
+        assertFalse(ifaces.contains(iface1.interfaceName))
+        assertTrue(ifaces.contains(iface2.interfaceName))
+
+        removeInterface(iface2)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/EthernetNetworkSpecifierTest.java b/tests/cts/net/src/android/net/cts/EthernetNetworkSpecifierTest.java
new file mode 100644
index 0000000..ef8fd1a
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/EthernetNetworkSpecifierTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.net.EthernetNetworkSpecifier;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner.class)
+public class EthernetNetworkSpecifierTest {
+
+    @Test
+    public void testConstructor() {
+        final String iface = "testIface";
+        final EthernetNetworkSpecifier ns = new EthernetNetworkSpecifier(iface);
+        assertEquals(iface, ns.getInterfaceName());
+    }
+
+    @Test
+    public void testConstructorWithNullValue() {
+        assertThrows("Should not be able to call constructor with null value.",
+                IllegalArgumentException.class,
+                () -> new EthernetNetworkSpecifier(null));
+    }
+
+    @Test
+    public void testConstructorWithEmptyValue() {
+        assertThrows("Should not be able to call constructor with empty value.",
+                IllegalArgumentException.class,
+                () -> new EthernetNetworkSpecifier(""));
+    }
+
+    @Test
+    public void testEquals() {
+        final String iface = "testIface";
+        final EthernetNetworkSpecifier nsOne = new EthernetNetworkSpecifier(iface);
+        final EthernetNetworkSpecifier nsTwo = new EthernetNetworkSpecifier(iface);
+        assertEquals(nsOne, nsTwo);
+    }
+
+    @Test
+    public void testNotEquals() {
+        final String iface = "testIface";
+        final String ifaceTwo = "testIfaceTwo";
+        final EthernetNetworkSpecifier nsOne = new EthernetNetworkSpecifier(iface);
+        final EthernetNetworkSpecifier nsTwo = new EthernetNetworkSpecifier(ifaceTwo);
+        assertNotEquals(nsOne, nsTwo);
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6e9f0cd..9590f88 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -17,16 +17,22 @@
 package android.net.cts;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
@@ -40,24 +46,38 @@
 import android.net.Ikev2VpnProfile;
 import android.net.IpSecAlgorithm;
 import android.net.Network;
-import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.ProxyInfo;
 import android.net.TestNetworkInterface;
 import android.net.VpnManager;
 import android.net.cts.util.CtsNetUtils;
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.os.Build;
 import android.os.Process;
 import android.platform.test.annotations.AppModeFull;
+import android.text.TextUtils;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.util.HexDump;
+import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
+import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
+import com.android.networkstack.apishim.VpnManagerShimImpl;
+import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
+import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.apishim.common.VpnManagerShim;
+import com.android.networkstack.apishim.common.VpnProfileStateShim;
+import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.RecorderCallback.CallbackEntry;
+import com.android.testutils.TestableNetworkCallback;
 
 import org.bouncycastle.x509.X509V1CertificateGenerator;
 import org.junit.After;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -80,6 +100,9 @@
 public class Ikev2VpnTest {
     private static final String TAG = Ikev2VpnTest.class.getSimpleName();
 
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
     // Test vectors for IKE negotiation in test mode.
     private static final String SUCCESSFUL_IKE_INIT_RESP_V4 =
             "46b8eca1e0d72a18b2b5d9006d47a0022120222000000000000002d0220000300000002c01010004030000"
@@ -167,6 +190,9 @@
     private static final VpnManager sVpnMgr =
             (VpnManager) sContext.getSystemService(Context.VPN_MANAGEMENT_SERVICE);
     private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
+    private static final long TIMEOUT_MS = 15_000;
+
+    private VpnManagerShim mVmShim = VpnManagerShimImpl.newInstance(sContext);
 
     private final X509Certificate mServerRootCa;
     private final CertificateAndKey mUserCertKey;
@@ -198,49 +224,56 @@
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileCommon(
-            Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks) throws Exception {
+            @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+            boolean requiresValidation) throws Exception {
+
+        builderShim.setBypassable(true)
+                .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
+                .setProxy(TEST_PROXY_INFO)
+                .setMaxMtu(TEST_MTU)
+                .setMetered(false);
+        if (TestUtils.shouldTestTApis()) {
+            builderShim.setRequiresInternetValidation(requiresValidation);
+        }
+
+        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
+        // method and does not defined in shims.
+        // TODO: replace it in alternative way to remove the hidden method usage
+        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
 
-        return builder.setBypassable(true)
-                .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
-                .setProxy(TEST_PROXY_INFO)
-                .setMaxMtu(TEST_MTU)
-                .setMetered(false)
-                .build();
+        return builder.build();
     }
 
-    private Ikev2VpnProfile buildIkev2VpnProfilePsk(boolean isRestrictedToTestNetworks)
-            throws Exception {
-        return buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6, isRestrictedToTestNetworks);
-    }
-
-    private Ikev2VpnProfile buildIkev2VpnProfilePsk(
-            String remote, boolean isRestrictedToTestNetworks) throws Exception {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
-
-        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+    private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
+            boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY, null)
+                        .setAuthPsk(TEST_PSK);
+        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
+                requiresValidation);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
-                        .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
 
-        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+                        .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
+        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
+                false /* requiresValidation */);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
                         .setAuthDigitalSignature(
                                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
-
-        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
+                false /* requiresValidation */);
     }
 
     private void checkBasicIkev2VpnProfile(@NonNull Ikev2VpnProfile profile) throws Exception {
@@ -254,12 +287,11 @@
         assertFalse(profile.isRestrictedToTestNetworks());
     }
 
-    @Test
-    public void testBuildIkev2VpnProfilePsk() throws Exception {
+    public void doTestBuildIkev2VpnProfilePsk(final boolean requiresValidation) throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
 
-        final Ikev2VpnProfile profile =
-                buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
+                false /* isRestrictedToTestNetworks */, requiresValidation);
 
         checkBasicIkev2VpnProfile(profile);
         assertArrayEquals(TEST_PSK, profile.getPresharedKey());
@@ -270,6 +302,46 @@
         assertNull(profile.getServerRootCaCert());
         assertNull(profile.getRsaPrivateKey());
         assertNull(profile.getUserCert());
+        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = new Ikev2VpnProfileShimImpl(profile);
+        if (TestUtils.shouldTestTApis()) {
+            assertEquals(requiresValidation, shim.isInternetValidationRequired());
+        } else {
+            try {
+                shim.isInternetValidationRequired();
+                fail("Only supported from API level 33");
+            } catch (UnsupportedApiLevelException expected) {
+            }
+        }
+    }
+
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testBuildIkev2VpnProfileWithIkeTunnelConnectionParams() throws Exception {
+        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+        assumeTrue(TestUtils.shouldTestTApis());
+
+        final IkeTunnelConnectionParams expectedParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+        final Ikev2VpnProfileBuilderShim ikeProfileBuilder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, expectedParams);
+        // Verify the other Ike options could not be set with IkeTunnelConnectionParams.
+        final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+        assertThrows(expected, () -> ikeProfileBuilder.setAuthPsk(TEST_PSK));
+        assertThrows(expected, () ->
+                ikeProfileBuilder.setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa));
+        assertThrows(expected, () -> ikeProfileBuilder.setAuthDigitalSignature(
+                mUserCertKey.cert, mUserCertKey.key, mServerRootCa));
+
+        final Ikev2VpnProfile profile = (Ikev2VpnProfile) ikeProfileBuilder.build().getProfile();
+
+        assertEquals(expectedParams,
+                new Ikev2VpnProfileShimImpl(profile).getIkeTunnelConnectionParams());
+    }
+
+    @Test
+    public void testBuildIkev2VpnProfilePsk() throws Exception {
+        doTestBuildIkev2VpnProfilePsk(true /* requiresValidation */);
+        doTestBuildIkev2VpnProfilePsk(false /* requiresValidation */);
     }
 
     @Test
@@ -316,8 +388,8 @@
         setAppop(AppOpsManager.OP_ACTIVATE_VPN, hasActivateVpn);
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, hasActivatePlatformVpn);
 
-        final Ikev2VpnProfile profile =
-                buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
+                false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
         final Intent intent = sVpnMgr.provisionVpnProfile(profile);
         assertEquals(expectIntent, intent != null);
     }
@@ -360,8 +432,8 @@
 
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
 
-        final Ikev2VpnProfile profile =
-                buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
+                false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
         assertNull(sVpnMgr.provisionVpnProfile(profile));
 
         // Verify that deleting the profile works (even without the appop)
@@ -394,7 +466,8 @@
         }
     }
 
-    private void checkStartStopVpnProfileBuildsNetworks(IkeTunUtils tunUtils, boolean testIpv6)
+    private void checkStartStopVpnProfileBuildsNetworks(@NonNull IkeTunUtils tunUtils,
+            boolean testIpv6, boolean requiresValidation, boolean testSessionKey)
             throws Exception {
         String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
         String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4;
@@ -404,11 +477,29 @@
         // Requires MANAGE_TEST_NETWORKS to provision a test-mode profile.
         mCtsNetUtils.setAppopPrivileged(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
 
-        final Ikev2VpnProfile profile =
-                buildIkev2VpnProfilePsk(serverAddr, true /* isRestrictedToTestNetworks */);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(serverAddr,
+                true /* isRestrictedToTestNetworks */, requiresValidation);
         assertNull(sVpnMgr.provisionVpnProfile(profile));
 
-        sVpnMgr.startProvisionedVpnProfile();
+        final TestableNetworkCallback cb = new TestableNetworkCallback(TIMEOUT_MS);
+        final NetworkRequest nr = new NetworkRequest.Builder()
+                .clearCapabilities().addTransportType(TRANSPORT_VPN).build();
+        sCM.registerNetworkCallback(nr, cb);
+
+        if (testSessionKey) {
+            // testSessionKey will never be true if running on <T
+            // startProvisionedVpnProfileSession() should return a non-null & non-empty random UUID.
+            final String sessionId = mVmShim.startProvisionedVpnProfileSession();
+            assertFalse(TextUtils.isEmpty(sessionId));
+            final VpnProfileStateShim profileState = mVmShim.getProvisionedVpnProfileState();
+            assertNotNull(profileState);
+            assertEquals(ConstantsShim.VPN_PROFILE_STATE_CONNECTING, profileState.getState());
+            assertEquals(sessionId, profileState.getSessionId());
+            assertFalse(profileState.isAlwaysOn());
+            assertFalse(profileState.isLockdownEnabled());
+        } else {
+            sVpnMgr.startProvisionedVpnProfile();
+        }
 
         // Inject IKE negotiation
         int expectedMsgId = 0;
@@ -418,35 +509,61 @@
                 HexDump.hexStringToByteArray(authResp));
 
         // Verify the VPN network came up
-        final NetworkRequest nr = new NetworkRequest.Builder()
-                .clearCapabilities().addTransportType(TRANSPORT_VPN).build();
+        final Network vpnNetwork = cb.expectCallback(CallbackEntry.AVAILABLE, anyNetwork())
+                .getNetwork();
 
-        final TestNetworkCallback cb = new TestNetworkCallback();
-        sCM.requestNetwork(nr, cb);
-        cb.waitForAvailable();
-        final Network vpnNetwork = cb.currentNetwork;
-        assertNotNull(vpnNetwork);
+        if (testSessionKey) {
+            final VpnProfileStateShim profileState = mVmShim.getProvisionedVpnProfileState();
+            assertNotNull(profileState);
+            assertEquals(ConstantsShim.VPN_PROFILE_STATE_CONNECTED, profileState.getState());
+            assertFalse(profileState.isAlwaysOn());
+            assertFalse(profileState.isLockdownEnabled());
+        }
 
-        final NetworkCapabilities caps = sCM.getNetworkCapabilities(vpnNetwork);
-        assertTrue(caps.hasTransport(TRANSPORT_VPN));
-        assertTrue(caps.hasCapability(NET_CAPABILITY_INTERNET));
-        assertEquals(Process.myUid(), caps.getOwnerUid());
+        cb.expectCapabilitiesThat(vpnNetwork, TIMEOUT_MS, caps -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasCapability(NET_CAPABILITY_INTERNET)
+                && !caps.hasCapability(NET_CAPABILITY_VALIDATED)
+                && Process.myUid() == caps.getOwnerUid());
+        cb.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, vpnNetwork);
+        cb.expectCallback(CallbackEntry.BLOCKED_STATUS, vpnNetwork);
+
+        // A VPN that requires validation is initially not validated, while one that doesn't
+        // immediately validate automatically. Because this VPN can't actually access Internet,
+        // the VPN only validates if it doesn't require validation. If the VPN requires validation
+        // but unexpectedly sends this callback, expecting LOST below will fail because the next
+        // callback will be the validated capabilities instead.
+        // In S and below, |requiresValidation| is ignored, so this callback is always sent
+        // regardless of its value.
+        if (!requiresValidation || !TestUtils.shouldTestTApis()) {
+            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, TIMEOUT_MS,
+                    entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                            .hasCapability(NET_CAPABILITY_VALIDATED));
+        }
 
         sVpnMgr.stopProvisionedVpnProfile();
-        cb.waitForLost();
-        assertEquals(vpnNetwork, cb.lastLostNetwork);
+        // Using expectCallback may cause the test to be flaky since test may receive other
+        // callbacks such as linkproperties change.
+        cb.eventuallyExpect(CallbackEntry.LOST, TIMEOUT_MS,
+                lost -> vpnNetwork.equals(lost.getNetwork()));
     }
 
     private class VerifyStartStopVpnProfileTest implements TestNetworkRunnable.Test {
         private final boolean mTestIpv6Only;
+        private final boolean mRequiresValidation;
+        private final boolean mTestSessionKey;
 
         /**
          * Constructs the test
          *
          * @param testIpv6Only if true, builds a IPv6-only test; otherwise builds a IPv4-only test
+         * @param requiresValidation whether this VPN should request platform validation
+         * @param testSessionKey if true, start VPN by calling startProvisionedVpnProfileSession()
          */
-        VerifyStartStopVpnProfileTest(boolean testIpv6Only) {
+        VerifyStartStopVpnProfileTest(boolean testIpv6Only, boolean requiresValidation,
+                boolean testSessionKey) {
             mTestIpv6Only = testIpv6Only;
+            mRequiresValidation = requiresValidation;
+            mTestSessionKey = testSessionKey;
         }
 
         @Override
@@ -454,7 +571,8 @@
                 throws Exception {
             final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
 
-            checkStartStopVpnProfileBuildsNetworks(tunUtils, mTestIpv6Only);
+            checkStartStopVpnProfileBuildsNetworks(
+                    tunUtils, mTestIpv6Only, mRequiresValidation, mTestSessionKey);
         }
 
         @Override
@@ -478,7 +596,14 @@
 
         // Requires shell permission to update appops.
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        false /* testIpv6Only */, false /* requiresValidation */,
+                        false /* testSessionKey */)));
+
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        false /* testIpv6Only */, true /* requiresValidation */,
+                        false /* testSessionKey */)));
     }
 
     @Test
@@ -487,7 +612,31 @@
 
         // Requires shell permission to update appops.
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        true /* testIpv6Only */, false /* requiresValidation */,
+                        false /* testSessionKey */)));
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        true /* testIpv6Only */, true /* requiresValidation */,
+                        false /* testSessionKey */)));
+    }
+
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testStartProvisionedVpnProfileSession() throws Exception {
+        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+        assumeTrue(TestUtils.shouldTestTApis());
+
+        // Requires shell permission to update appops.
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        false /* testIpv6Only */, false /* requiresValidation */,
+                        true /* testSessionKey */)));
+
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        true /* testIpv6Only */, false /* requiresValidation */,
+                        true /* testSessionKey */)));
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/IpConfigurationTest.java b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java
index 23744eb..1d19d26 100644
--- a/tests/cts/net/src/android/net/cts/IpConfigurationTest.java
+++ b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java
@@ -107,7 +107,11 @@
         assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
     }
 
-    @ConnectivityModuleTest @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    @ConnectivityModuleTest // The builder was added in an S+ module update.
+    // This whole class is not skipped (marked @ConnectivityModuleTest) in MTS for non-connectivity
+    // modules like NetworkStack, as NetworkStack uses IpConfiguration a lot on Q+, so tests that
+    // cover older APIs are still useful to provide used API coverage for NetworkStack.
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
     @Test
     public void testBuilder() {
         final IpConfiguration c = new IpConfiguration.Builder()
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index 7bce3d2..8234ec1 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -52,6 +52,7 @@
 
 import static com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel;
 import static com.android.compatibility.common.util.PropertyUtil.getVendorApiLevel;
+import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -129,12 +130,11 @@
             assertTrue("Failed to allocate specified SPI, " + DROID_SPI,
                     droidSpi.getSpi() == DROID_SPI);
 
-            try {
-                mISM.allocateSecurityParameterIndex(addr, DROID_SPI);
-                fail("Duplicate SPI was allowed to be created");
-            } catch (IpSecManager.SpiUnavailableException expected) {
-                // This is a success case because we expect a dupe SPI to throw
-            }
+            IpSecManager.SpiUnavailableException expectedException =
+                    assertThrows("Duplicate SPI was allowed to be created",
+                            IpSecManager.SpiUnavailableException.class,
+                            () -> mISM.allocateSecurityParameterIndex(addr, DROID_SPI));
+            assertEquals(expectedException.getSpi(), droidSpi.getSpi());
 
             randomSpi.close();
             droidSpi.close();
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index bf97339..0504973 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -19,6 +19,7 @@
 import android.app.Instrumentation
 import android.content.Context
 import android.net.ConnectivityManager
+import android.net.EthernetNetworkSpecifier
 import android.net.INetworkAgent
 import android.net.INetworkAgentRegistry
 import android.net.InetAddresses
@@ -35,6 +36,7 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
@@ -42,7 +44,9 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkCapabilities.TRANSPORT_VPN
 import android.net.NetworkInfo
 import android.net.NetworkProvider
@@ -68,6 +72,7 @@
 import android.os.HandlerThread
 import android.os.Message
 import android.os.SystemClock
+import android.platform.test.annotations.AppModeFull
 import android.telephony.TelephonyManager
 import android.telephony.data.EpsBearerQosSessionAttributes
 import android.util.DebugUtils.valueToString
@@ -100,8 +105,8 @@
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
 import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.assertThrows
 import org.junit.After
-import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -112,6 +117,8 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.timeout
 import org.mockito.Mockito.verify
+import java.io.IOException
+import java.net.DatagramSocket
 import java.net.InetAddress
 import java.net.InetSocketAddress
 import java.net.Socket
@@ -154,6 +161,10 @@
 // NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
 // versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
 @IgnoreUpTo(Build.VERSION_CODES.R)
+// NetworkAgent is updated as part of the connectivity module, and running NetworkAgent tests in MTS
+// for modules other than Connectivity does not provide much value. Only run them in connectivity
+// module MTS, so the tests only need to cover the case of an updated NetworkAgent.
+@ConnectivityModuleTest
 class NetworkAgentTest {
     private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
     private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
@@ -245,6 +256,28 @@
                 .build()
     }
 
+    private fun makeTestNetworkCapabilities(
+        specifier: String? = null,
+        transports: IntArray = intArrayOf()
+    ) = NetworkCapabilities().apply {
+        addTransportType(TRANSPORT_TEST)
+        removeCapability(NET_CAPABILITY_TRUSTED)
+        removeCapability(NET_CAPABILITY_INTERNET)
+        addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+        addCapability(NET_CAPABILITY_NOT_ROAMING)
+        addCapability(NET_CAPABILITY_NOT_VPN)
+        if (SdkLevel.isAtLeastS()) {
+            addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        }
+        if (null != specifier) {
+            setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier))
+        }
+        for (t in transports) { addTransportType(t) }
+        // Most transports are not allowed on test networks unless the network is marked restricted.
+        // This test does not need
+        if (transports.size > 0) removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+    }
+
     private fun createNetworkAgent(
         context: Context = realContext,
         specifier: String? = null,
@@ -252,20 +285,7 @@
         initialLp: LinkProperties? = null,
         initialConfig: NetworkAgentConfig? = null
     ): TestableNetworkAgent {
-        val nc = initialNc ?: NetworkCapabilities().apply {
-            addTransportType(TRANSPORT_TEST)
-            removeCapability(NET_CAPABILITY_TRUSTED)
-            removeCapability(NET_CAPABILITY_INTERNET)
-            addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-            addCapability(NET_CAPABILITY_NOT_ROAMING)
-            addCapability(NET_CAPABILITY_NOT_VPN)
-            if (SdkLevel.isAtLeastS()) {
-                addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-            }
-            if (null != specifier) {
-                setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier))
-            }
-        }
+        val nc = initialNc ?: makeTestNetworkCapabilities(specifier)
         val lp = initialLp ?: LinkProperties().apply {
             addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
             addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
@@ -280,12 +300,14 @@
         context: Context = realContext,
         specifier: String? = UUID.randomUUID().toString(),
         initialConfig: NetworkAgentConfig? = null,
-        expectedInitSignalStrengthThresholds: IntArray? = intArrayOf()
+        expectedInitSignalStrengthThresholds: IntArray? = intArrayOf(),
+        transports: IntArray = intArrayOf()
     ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
         val callback = TestableNetworkCallback()
         // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
         requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
-        val agent = createNetworkAgent(context, specifier, initialConfig = initialConfig)
+        val nc = makeTestNetworkCapabilities(specifier, transports)
+        val agent = createNetworkAgent(context, initialConfig = initialConfig, initialNc = nc)
         agent.setTeardownDelayMillis(0)
         // Connect the agent and verify initial status callbacks.
         agent.register()
@@ -297,6 +319,15 @@
         return agent to callback
     }
 
+    private fun connectNetwork(vararg transports: Int): Pair<TestableNetworkAgent, Network> {
+        val (agent, callback) = createConnectedNetworkAgent(transports = transports)
+        val network = agent.network!!
+        // createConnectedNetworkAgent internally files a request; release it so that the network
+        // will be torn down if unneeded.
+        mCM.unregisterNetworkCallback(callback)
+        return agent to network
+    }
+
     private fun createNetworkAgentWithFakeCS() = createNetworkAgent().also {
         mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID)))
     }
@@ -462,34 +493,33 @@
         }
     }
 
-    private fun ncWithAccessUids(vararg uids: Int) = NetworkCapabilities.Builder()
+    private fun ncWithAllowedUids(vararg uids: Int) = NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_TEST)
-                .setAccessUids(uids.toSet()).build()
+                .setAllowedUids(uids.toSet()).build()
 
-    @ConnectivityModuleTest // Functionality was added post-S via connectivity module update
     @Test
     fun testRejectedUpdates() {
-        val callback = TestableNetworkCallback()
+        val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
         // will be cleaned up in tearDown
         registerNetworkCallback(makeTestNetworkRequest(), callback)
-        val agent = createNetworkAgent(initialNc = ncWithAccessUids(200))
+        val agent = createNetworkAgent(initialNc = ncWithAllowedUids(200))
         agent.register()
         agent.markConnected()
 
         // Make sure the UIDs have been ignored.
         callback.expectCallback<Available>(agent.network!!)
         callback.expectCapabilitiesThat(agent.network!!) {
-            it.accessUids.isEmpty() && !it.hasCapability(NET_CAPABILITY_VALIDATED)
+            it.allowedUids.isEmpty() && !it.hasCapability(NET_CAPABILITY_VALIDATED)
         }
         callback.expectCallback<LinkPropertiesChanged>(agent.network!!)
         callback.expectCallback<BlockedStatus>(agent.network!!)
         callback.expectCapabilitiesThat(agent.network!!) {
-            it.accessUids.isEmpty() && it.hasCapability(NET_CAPABILITY_VALIDATED)
+            it.allowedUids.isEmpty() && it.hasCapability(NET_CAPABILITY_VALIDATED)
         }
         callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
 
         // Make sure that the UIDs are also ignored upon update
-        agent.sendNetworkCapabilities(ncWithAccessUids(200, 300))
+        agent.sendNetworkCapabilities(ncWithAllowedUids(200, 300))
         callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
     }
 
@@ -916,11 +946,9 @@
         return Pair(agent, qosTestSocket!!)
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackRegisterWithUnregister() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
 
         val qosCallback = TestableQosCallback()
@@ -945,11 +973,9 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackOnQosSession() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
         val qosCallback = TestableQosCallback()
         Executors.newSingleThreadExecutor().let { executor ->
@@ -993,11 +1019,9 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackOnError() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
         val qosCallback = TestableQosCallback()
         Executors.newSingleThreadExecutor().let { executor ->
@@ -1034,11 +1058,9 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackIdsAreMappedCorrectly() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
         val qosCallback1 = TestableQosCallback()
         val qosCallback2 = TestableQosCallback()
@@ -1077,11 +1099,9 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackWhenNetworkReleased() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
         Executors.newSingleThreadExecutor().let { executor ->
             try {
@@ -1120,4 +1140,139 @@
                 remoteAddresses
         )
     }
+
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
+    @Test
+    fun testUnregisterAfterReplacement() {
+        // Keeps an eye on all test networks.
+        val matchAllCallback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+        registerNetworkCallback(makeTestNetworkRequest(), matchAllCallback)
+
+        // File a request that matches and keeps up the best-scoring test network.
+        val testCallback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+        requestNetwork(makeTestNetworkRequest(), testCallback)
+
+        // Connect the first network. This should satisfy the request.
+        val (agent1, network1) = connectNetwork()
+        matchAllCallback.expectAvailableThenValidatedCallbacks(network1)
+        testCallback.expectAvailableThenValidatedCallbacks(network1)
+        // Check that network1 exists by binding a socket to it and getting no exceptions.
+        network1.bindSocket(DatagramSocket())
+
+        // Connect a second agent. network1 is preferred because it was already registered, so
+        // testCallback will not see any events. agent2 is be torn down because it has no requests.
+        val (agent2, network2) = connectNetwork()
+        matchAllCallback.expectAvailableThenValidatedCallbacks(network2)
+        matchAllCallback.expectCallback<Lost>(network2)
+        agent2.expectCallback<OnNetworkUnwanted>()
+        agent2.expectCallback<OnNetworkDestroyed>()
+        assertNull(mCM.getLinkProperties(network2))
+
+        // Mark the first network as awaiting replacement. This should destroy the underlying
+        // native network and send onNetworkDestroyed, but will not send any NetworkCallbacks,
+        // because for callback and scoring purposes network1 is still connected.
+        agent1.unregisterAfterReplacement(5_000 /* timeoutMillis */)
+        agent1.expectCallback<OnNetworkDestroyed>()
+        assertThrows(IOException::class.java) { network1.bindSocket(DatagramSocket()) }
+        assertNotNull(mCM.getLinkProperties(network1))
+
+        // Calling unregisterAfterReplacement more than once has no effect.
+        // If it did, this test would fail because the 1ms timeout means that the network would be
+        // torn down before the replacement arrives.
+        agent1.unregisterAfterReplacement(1 /* timeoutMillis */)
+
+        // Connect a third network. Because network1 is awaiting replacement, network3 is preferred
+        // as soon as it validates (until then, it is outscored by network1).
+        // The fact that the first events seen by matchAllCallback is the connection of network3
+        // implicitly ensures that no callbacks are sent since network1 was lost.
+        val (agent3, network3) = connectNetwork()
+        matchAllCallback.expectAvailableThenValidatedCallbacks(network3)
+        testCallback.expectAvailableDoubleValidatedCallbacks(network3)
+
+        // As soon as the replacement arrives, network1 is disconnected.
+        // Check that this happens before the replacement timeout (5 seconds) fires.
+        matchAllCallback.expectCallback<Lost>(network1, 2_000 /* timeoutMs */)
+        agent1.expectCallback<OnNetworkUnwanted>()
+
+        // Test lingering:
+        // - Connect a higher-scoring network and check that network3 starts lingering.
+        // - Mark network3 awaiting replacement.
+        // - Check that network3 is torn down immediately without waiting for the linger timer or
+        //   the replacement timer to fire. This is a regular teardown, so it results in
+        //   onNetworkUnwanted before onNetworkDestroyed.
+        val (agent4, agent4callback) = createConnectedNetworkAgent()
+        val network4 = agent4.network!!
+        matchAllCallback.expectAvailableThenValidatedCallbacks(network4)
+        agent4.sendNetworkScore(NetworkScore.Builder().setTransportPrimary(true).build())
+        matchAllCallback.expectCallback<Losing>(network3)
+        testCallback.expectAvailableCallbacks(network4, validated = true)
+        mCM.unregisterNetworkCallback(agent4callback)
+        agent3.unregisterAfterReplacement(5_000)
+        agent3.expectCallback<OnNetworkUnwanted>()
+        matchAllCallback.expectCallback<Lost>(network3, 1000L)
+        agent3.expectCallback<OnNetworkDestroyed>()
+
+        // Now mark network4 awaiting replacement with a low timeout, and check that if no
+        // replacement arrives, it is torn down.
+        agent4.unregisterAfterReplacement(100 /* timeoutMillis */)
+        matchAllCallback.expectCallback<Lost>(network4, 1000L /* timeoutMs */)
+        testCallback.expectCallback<Lost>(network4, 1000L /* timeoutMs */)
+        agent4.expectCallback<OnNetworkDestroyed>()
+        agent4.expectCallback<OnNetworkUnwanted>()
+
+        // If a network that is awaiting replacement is unregistered, it disconnects immediately,
+        // before the replacement timeout fires.
+        val (agent5, network5) = connectNetwork()
+        matchAllCallback.expectAvailableThenValidatedCallbacks(network5)
+        testCallback.expectAvailableThenValidatedCallbacks(network5)
+        agent5.unregisterAfterReplacement(5_000 /* timeoutMillis */)
+        agent5.unregister()
+        matchAllCallback.expectCallback<Lost>(network5, 1000L /* timeoutMs */)
+        testCallback.expectCallback<Lost>(network5, 1000L /* timeoutMs */)
+        agent5.expectCallback<OnNetworkDestroyed>()
+        agent5.expectCallback<OnNetworkUnwanted>()
+
+        // If wifi is replaced within the timeout, the device does not switch to cellular.
+        val (cellAgent, cellNetwork) = connectNetwork(TRANSPORT_CELLULAR)
+        testCallback.expectAvailableThenValidatedCallbacks(cellNetwork)
+        matchAllCallback.expectAvailableThenValidatedCallbacks(cellNetwork)
+
+        val (wifiAgent, wifiNetwork) = connectNetwork(TRANSPORT_WIFI)
+        testCallback.expectAvailableCallbacks(wifiNetwork, validated = true)
+        testCallback.expectCapabilitiesThat(wifiNetwork) {
+            it.hasCapability(NET_CAPABILITY_VALIDATED)
+        }
+        matchAllCallback.expectAvailableCallbacks(wifiNetwork, validated = false)
+        matchAllCallback.expectCallback<Losing>(cellNetwork)
+        matchAllCallback.expectCapabilitiesThat(wifiNetwork) {
+            it.hasCapability(NET_CAPABILITY_VALIDATED)
+        }
+
+        wifiAgent.unregisterAfterReplacement(5_000 /* timeoutMillis */)
+        wifiAgent.expectCallback<OnNetworkDestroyed>()
+
+        // Once the network is awaiting replacement, changing LinkProperties, NetworkCapabilities or
+        // score, or calling reportNetworkConnectivity, have no effect.
+        val wifiSpecifier = mCM.getNetworkCapabilities(wifiNetwork)!!.networkSpecifier
+        assertNotNull(wifiSpecifier)
+        assertTrue(wifiSpecifier is EthernetNetworkSpecifier)
+
+        val wifiNc = makeTestNetworkCapabilities(wifiSpecifier.interfaceName,
+                intArrayOf(TRANSPORT_WIFI))
+        wifiAgent.sendNetworkCapabilities(wifiNc)
+        val wifiLp = mCM.getLinkProperties(wifiNetwork)!!
+        val newRoute = RouteInfo(IpPrefix("192.0.2.42/24"))
+        assertFalse(wifiLp.getRoutes().contains(newRoute))
+        wifiLp.addRoute(newRoute)
+        wifiAgent.sendLinkProperties(wifiLp)
+        mCM.reportNetworkConnectivity(wifiNetwork, false)
+        // The test implicitly checks that no callbacks are sent here, because the next events seen
+        // by the callbacks are for the new network connecting.
+
+        val (newWifiAgent, newWifiNetwork) = connectNetwork(TRANSPORT_WIFI)
+        testCallback.expectAvailableCallbacks(newWifiNetwork, validated = true)
+        matchAllCallback.expectAvailableThenValidatedCallbacks(newWifiNetwork)
+        matchAllCallback.expectCallback<Lost>(wifiNetwork)
+        wifiAgent.expectCallback<OnNetworkUnwanted>()
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index 147fca9..fb720a7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -21,6 +21,9 @@
 import static android.app.usage.NetworkStats.Bucket.METERED_ALL;
 import static android.app.usage.NetworkStats.Bucket.METERED_NO;
 import static android.app.usage.NetworkStats.Bucket.METERED_YES;
+import static android.app.usage.NetworkStats.Bucket.ROAMING_ALL;
+import static android.app.usage.NetworkStats.Bucket.ROAMING_NO;
+import static android.app.usage.NetworkStats.Bucket.ROAMING_YES;
 import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
 import static android.app.usage.NetworkStats.Bucket.STATE_DEFAULT;
 import static android.app.usage.NetworkStats.Bucket.STATE_FOREGROUND;
@@ -77,6 +80,7 @@
 
     private abstract class NetworkInterfaceToTest {
         private boolean mMetered;
+        private boolean mRoaming;
         private boolean mIsDefault;
 
         abstract int getNetworkType();
@@ -90,6 +94,14 @@
             this.mMetered = metered;
         }
 
+        public boolean getRoaming() {
+            return mRoaming;
+        }
+
+        public void setRoaming(boolean roaming) {
+            this.mRoaming = roaming;
+        }
+
         public boolean getIsDefault() {
             return mIsDefault;
         }
@@ -267,6 +279,7 @@
         private URL mUrl;
         public boolean success;
         public boolean metered;
+        public boolean roaming;
         public boolean isDefault;
 
         NetworkCallback(long tolerance, URL url) {
@@ -274,6 +287,7 @@
             mUrl = url;
             success = false;
             metered = false;
+            roaming = false;
             isDefault = false;
         }
 
@@ -303,6 +317,8 @@
                 success = true;
                 metered = !mCm.getNetworkCapabilities(network)
                         .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+                roaming = !mCm.getNetworkCapabilities(network)
+                        .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
                 synchronized (NetworkStatsManagerTest.this) {
                     NetworkStatsManagerTest.this.notify();
                 }
@@ -333,6 +349,7 @@
         }
         if (callback.success) {
             mNetworkInterfacesToTest[networkTypeIndex].setMetered(callback.metered);
+            mNetworkInterfacesToTest[networkTypeIndex].setRoaming(callback.roaming);
             mNetworkInterfacesToTest[networkTypeIndex].setIsDefault(callback.isDefault);
             return true;
         }
@@ -377,6 +394,7 @@
             assertEquals(bucket.getState(), STATE_ALL);
             assertEquals(bucket.getUid(), UID_ALL);
             assertEquals(bucket.getMetered(), METERED_ALL);
+            assertEquals(bucket.getRoaming(), ROAMING_ALL);
             assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL);
             setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny");
             try {
@@ -412,6 +430,7 @@
             assertEquals(bucket.getState(), STATE_ALL);
             assertEquals(bucket.getUid(), UID_ALL);
             assertEquals(bucket.getMetered(), METERED_ALL);
+            assertEquals(bucket.getRoaming(), ROAMING_ALL);
             assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL);
             setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny");
             try {
@@ -451,15 +470,19 @@
                 long totalTxBytes = 0;
                 long totalRxBytes = 0;
                 boolean hasCorrectMetering = false;
+                boolean hasCorrectRoaming = false;
                 boolean hasCorrectDefaultStatus = false;
                 int expectedMetering = mNetworkInterfacesToTest[i].getMetered()
                         ? METERED_YES : METERED_NO;
+                int expectedRoaming = mNetworkInterfacesToTest[i].getRoaming()
+                        ? ROAMING_YES : ROAMING_NO;
                 int expectedDefaultStatus = mNetworkInterfacesToTest[i].getIsDefault()
                         ? DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO;
                 while (result.hasNextBucket()) {
                     assertTrue(result.getNextBucket(bucket));
                     assertTimestamps(bucket);
                     hasCorrectMetering |= bucket.getMetered() == expectedMetering;
+                    hasCorrectRoaming |= bucket.getRoaming() == expectedRoaming;
                     if (bucket.getUid() == Process.myUid()) {
                         totalTxPackets += bucket.getTxPackets();
                         totalRxPackets += bucket.getRxPackets();
@@ -472,6 +495,8 @@
                 assertFalse(result.getNextBucket(bucket));
                 assertTrue("Incorrect metering for NetworkType: "
                         + mNetworkInterfacesToTest[i].getNetworkType(), hasCorrectMetering);
+                assertTrue("Incorrect roaming for NetworkType: "
+                        + mNetworkInterfacesToTest[i].getNetworkType(), hasCorrectRoaming);
                 assertTrue("Incorrect isDefault for NetworkType: "
                         + mNetworkInterfacesToTest[i].getNetworkType(), hasCorrectDefaultStatus);
                 assertTrue("No Rx bytes usage for uid " + Process.myUid(), totalRxBytes > 0);
@@ -564,6 +589,7 @@
                     assertTimestamps(bucket);
                     assertEquals(bucket.getState(), STATE_ALL);
                     assertEquals(bucket.getMetered(), METERED_ALL);
+                    assertEquals(bucket.getRoaming(), ROAMING_ALL);
                     assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL);
                     assertEquals(bucket.getUid(), Process.myUid());
                     totalTxPackets += bucket.getTxPackets();
@@ -617,6 +643,7 @@
                     assertTimestamps(bucket);
                     assertEquals(bucket.getState(), STATE_ALL);
                     assertEquals(bucket.getMetered(), METERED_ALL);
+                    assertEquals(bucket.getRoaming(), ROAMING_ALL);
                     assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL);
                     assertEquals(bucket.getUid(), Process.myUid());
                     if (bucket.getTag() == NETWORK_TAG) {
@@ -838,6 +865,7 @@
             if (expectedTag != null) assertEquals(bucket.getTag(), (int) expectedTag);
             if (expectedState != null) assertEquals(bucket.getState(), (int) expectedState);
             assertEquals(bucket.getMetered(), METERED_ALL);
+            assertEquals(bucket.getRoaming(), ROAMING_ALL);
             assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL);
             if (bucket.getUid() == Process.myUid()) {
                 totalTxPackets += bucket.getTxPackets();
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 9506081..b139a9b 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -71,6 +71,7 @@
 import java.net.ServerSocket
 import java.nio.charset.StandardCharsets
 import java.util.Random
+import java.util.concurrent.Executor
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertNotNull
@@ -342,9 +343,12 @@
         if (DBG) Log.d(TAG, "Port = $localPort")
 
         val registrationRecord = NsdRegistrationRecord()
-        val registeredInfo = registerService(registrationRecord, si)
+        // Test registering without an Executor
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+        val registeredInfo = registrationRecord.expectCallback<ServiceRegistered>().serviceInfo
 
         val discoveryRecord = NsdDiscoveryRecord()
+        // Test discovering without an Executor
         nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
 
         // Expect discovery started
@@ -353,7 +357,10 @@
         // Expect a service record to be discovered
         val foundInfo = discoveryRecord.waitForServiceDiscovered(registeredInfo.serviceName)
 
-        val resolvedService = resolveService(foundInfo)
+        // Test resolving without an Executor
+        val resolveRecord = NsdResolveRecord()
+        nsdManager.resolveService(foundInfo, resolveRecord)
+        val resolvedService = resolveRecord.expectCallback<ServiceResolved>().serviceInfo
 
         // Check Txt attributes
         assertEquals(8, resolvedService.attributes.size)
@@ -408,7 +415,7 @@
 
     @Test
     fun testNsdManager_DiscoverOnNetwork() {
-        // This tests requires shims supporting T+ APIs (discovering on specific network)
+        // This test requires shims supporting T+ APIs (discovering on specific network)
         assumeTrue(ConstantsShim.VERSION > SC_V2)
 
         val si = NsdServiceInfo()
@@ -422,7 +429,7 @@
         tryTest {
             val discoveryRecord = NsdDiscoveryRecord()
             nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
-                    testNetwork1.network, discoveryRecord)
+                    testNetwork1.network, Executor { it.run() }, discoveryRecord)
 
             val foundInfo = discoveryRecord.waitForServiceDiscovered(
                     serviceName, testNetwork1.network)
@@ -442,7 +449,7 @@
 
     @Test
     fun testNsdManager_DiscoverWithNetworkRequest() {
-        // This tests requires shims supporting T+ APIs (discovering on network request)
+        // This test requires shims supporting T+ APIs (discovering on network request)
         assumeTrue(ConstantsShim.VERSION > SC_V2)
 
         val si = NsdServiceInfo()
@@ -462,7 +469,7 @@
                             .addTransportType(TRANSPORT_TEST)
                             .setNetworkSpecifier(specifier)
                             .build(),
-                    discoveryRecord)
+                    Executor { it.run() }, discoveryRecord)
 
             val discoveryStarted = discoveryRecord.expectCallback<DiscoveryStarted>()
             assertEquals(SERVICE_TYPE, discoveryStarted.serviceType)
@@ -507,7 +514,7 @@
 
     @Test
     fun testNsdManager_ResolveOnNetwork() {
-        // This tests requires shims supporting T+ APIs (NsdServiceInfo.network)
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
         assumeTrue(ConstantsShim.VERSION > SC_V2)
 
         val si = NsdServiceInfo()
@@ -532,7 +539,7 @@
                     serviceName, testNetwork2.network)
             assertEquals(testNetwork2.network, nsdShim.getNetwork(foundInfo2))
 
-            nsdManager.resolveService(foundInfo1, resolveRecord)
+            nsdShim.resolveService(nsdManager, foundInfo1, Executor { it.run() }, resolveRecord)
             val cb = resolveRecord.expectCallback<ServiceResolved>()
             cb.serviceInfo.let {
                 // Resolved service type has leading dot
@@ -553,7 +560,8 @@
      * Register a service and return its registration record.
      */
     private fun registerService(record: NsdRegistrationRecord, si: NsdServiceInfo): NsdServiceInfo {
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, record)
+        nsdShim.registerService(nsdManager, si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() },
+                record)
         // We may not always get the name that we tried to register;
         // This events tells us the name that was registered.
         val cb = record.expectCallback<ServiceRegistered>()
@@ -562,7 +570,7 @@
 
     private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
         val record = NsdResolveRecord()
-        nsdManager.resolveService(discoveredInfo, record)
+        nsdShim.resolveService(nsdManager, discoveredInfo, Executor { it.run() }, record)
         val resolvedCb = record.expectCallback<ServiceResolved>()
         assertEquals(discoveredInfo.serviceName, resolvedCb.serviceInfo.serviceName)
 
diff --git a/tests/cts/net/src/android/net/cts/QosCallbackExceptionTest.java b/tests/cts/net/src/android/net/cts/QosCallbackExceptionTest.java
new file mode 100644
index 0000000..cd43a34
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/QosCallbackExceptionTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.NetworkReleasedException;
+import android.net.QosCallbackException;
+import android.net.SocketLocalAddressChangedException;
+import android.net.SocketNotBoundException;
+import android.os.Build;
+
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+public class QosCallbackExceptionTest {
+    private static final String ERROR_MESSAGE = "Test Error Message";
+    private static final String ERROR_MSG_SOCK_NOT_BOUND = "The socket is unbound";
+    private static final String ERROR_MSG_NET_RELEASED =
+            "The network was released and is no longer available";
+    private static final String ERROR_MSG_SOCK_ADDR_CHANGED =
+            "The local address of the socket changed";
+
+
+    @Test
+    public void testQosCallbackException() throws Exception {
+        final Throwable testcause = new Throwable(ERROR_MESSAGE);
+        final QosCallbackException exception = new QosCallbackException(testcause);
+        assertEquals(testcause, exception.getCause());
+
+        final QosCallbackException exceptionMsg = new QosCallbackException(ERROR_MESSAGE);
+        assertEquals(ERROR_MESSAGE, exceptionMsg.getMessage());
+    }
+
+    @Test
+    public void testNetworkReleasedExceptions() throws Exception {
+        final Throwable netReleasedException = new NetworkReleasedException();
+        final QosCallbackException exception = new QosCallbackException(netReleasedException);
+
+        assertTrue(exception.getCause() instanceof NetworkReleasedException);
+        assertEquals(netReleasedException, exception.getCause());
+        assertTrue(exception.getMessage().contains(ERROR_MSG_NET_RELEASED));
+        assertThrowableMessageContains(exception, ERROR_MSG_NET_RELEASED);
+    }
+
+    @Test
+    public void testSocketNotBoundExceptions() throws Exception {
+        final Throwable sockNotBoundException = new SocketNotBoundException();
+        final QosCallbackException exception = new QosCallbackException(sockNotBoundException);
+
+        assertTrue(exception.getCause() instanceof SocketNotBoundException);
+        assertEquals(sockNotBoundException, exception.getCause());
+        assertTrue(exception.getMessage().contains(ERROR_MSG_SOCK_NOT_BOUND));
+        assertThrowableMessageContains(exception, ERROR_MSG_SOCK_NOT_BOUND);
+    }
+
+    @Test
+    public void testSocketLocalAddressChangedExceptions() throws  Exception {
+        final Throwable localAddrChangedException = new SocketLocalAddressChangedException();
+        final QosCallbackException exception = new QosCallbackException(localAddrChangedException);
+
+        assertTrue(exception.getCause() instanceof SocketLocalAddressChangedException);
+        assertEquals(localAddrChangedException, exception.getCause());
+        assertTrue(exception.getMessage().contains(ERROR_MSG_SOCK_ADDR_CHANGED));
+        assertThrowableMessageContains(exception, ERROR_MSG_SOCK_ADDR_CHANGED);
+    }
+
+    private void assertThrowableMessageContains(QosCallbackException exception, String errorMsg)
+            throws Exception {
+        try {
+            triggerException(exception);
+            fail("Expect exception");
+        } catch (QosCallbackException e) {
+            assertTrue(e.getMessage().contains(errorMsg));
+        }
+    }
+
+    private void triggerException(QosCallbackException exception) throws Exception {
+        throw new QosCallbackException(exception.getCause());
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/RateLimitTest.java b/tests/cts/net/src/android/net/cts/RateLimitTest.java
new file mode 100644
index 0000000..423f213
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/RateLimitTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.icu.text.MessageFormat;
+import android.net.ConnectivityManager;
+import android.net.ConnectivitySettingsManager;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.RouteInfo;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.TestNetworkSpecifier;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.net.module.util.PacketBuilder;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestableNetworkAgent;
+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.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.stream.Collectors;
+
+@AppModeFull(reason = "Instant apps cannot access /dev/tun, so createTunInterface fails")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class RateLimitTest {
+    // cannot be final as it gets initialized inside ensureKernelConfigLoaded().
+    private static HashSet<String> sKernelConfig;
+
+    private static final String TAG = "RateLimitTest";
+    private static final LinkAddress LOCAL_IP4_ADDR = new LinkAddress("10.0.0.1/8");
+    private static final InetAddress REMOTE_IP4_ADDR = InetAddresses.parseNumericAddress("8.8.8.8");
+    private static final short TEST_UDP_PORT = 1234;
+    private static final byte TOS = 0;
+    private static final short ID = 27149;
+    private static final short DONT_FRAG_FLAG_MASK = (short) 0x4000; // flags=DF, offset=0
+    private static final byte TIME_TO_LIVE = 64;
+    private static final byte[] PAYLOAD = new byte[1472];
+
+    private Handler mHandler;
+    private Context mContext;
+    private TestNetworkManager mNetworkManager;
+    private TestNetworkInterface mTunInterface;
+    private ConnectivityManager mCm;
+    private TestNetworkSpecifier mNetworkSpecifier;
+    private NetworkCapabilities mNetworkCapabilities;
+    private TestableNetworkCallback mNetworkCallback;
+    private LinkProperties mLinkProperties;
+    private TestableNetworkAgent mNetworkAgent;
+    private Network mNetwork;
+    private DatagramSocket mSocket;
+
+    // Note: exceptions thrown in @BeforeClass or @ClassRule methods are not reported correctly.
+    // This function is called from setUp and loads the kernel config options the first time it is
+    // invoked. This ensures proper error reporting.
+    private static synchronized void ensureKernelConfigLoaded() {
+        if (sKernelConfig != null) return;
+        final String result = SystemUtil.runShellCommandOrThrow("gzip -cd /proc/config.gz");
+        sKernelConfig = Arrays.stream(result.split("\\R")).collect(
+                Collectors.toCollection(HashSet::new));
+
+        // make sure that if for some reason /proc/config.gz returns an empty string, this test
+        // does not silently fail.
+        assertNotEquals("gzip -cd /proc/config.gz returned an empty string", 0, result.length());
+    }
+
+    private static void assumeKernelSupport() {
+        assumeTrue(sKernelConfig.contains("CONFIG_NET_CLS_MATCHALL=y"));
+        assumeTrue(sKernelConfig.contains("CONFIG_NET_ACT_POLICE=y"));
+        assumeTrue(sKernelConfig.contains("CONFIG_NET_ACT_BPF=y"));
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        ensureKernelConfigLoaded();
+
+        mHandler = new Handler(Looper.getMainLooper());
+
+        runAsShell(MANAGE_TEST_NETWORKS, () -> {
+            mContext = getContext();
+
+            mNetworkManager = mContext.getSystemService(TestNetworkManager.class);
+            mTunInterface = mNetworkManager.createTunInterface(Arrays.asList(LOCAL_IP4_ADDR));
+        });
+
+        mCm = mContext.getSystemService(ConnectivityManager.class);
+        mNetworkSpecifier = new TestNetworkSpecifier(mTunInterface.getInterfaceName());
+        mNetworkCapabilities = new NetworkCapabilities.Builder()
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                .setNetworkSpecifier(mNetworkSpecifier).build();
+        mNetworkCallback = new TestableNetworkCallback();
+
+        mCm.requestNetwork(
+                new NetworkRequest.Builder()
+                        .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                        .setNetworkSpecifier(mNetworkSpecifier)
+                        .build(),
+                mNetworkCallback);
+
+        mLinkProperties = new LinkProperties();
+        mLinkProperties.addLinkAddress(LOCAL_IP4_ADDR);
+        mLinkProperties.setInterfaceName(mTunInterface.getInterfaceName());
+        mLinkProperties.addRoute(
+                new RouteInfo(new IpPrefix(IPV4_ADDR_ANY, 0), null,
+                        mTunInterface.getInterfaceName()));
+
+
+        runAsShell(MANAGE_TEST_NETWORKS, () -> {
+            mNetworkAgent = new TestableNetworkAgent(mContext, mHandler.getLooper(),
+                    mNetworkCapabilities, mLinkProperties,
+                    new NetworkAgentConfig.Builder().setExplicitlySelected(
+                            true).setUnvalidatedConnectivityAcceptable(true).build());
+
+            mNetworkAgent.register();
+            mNetworkAgent.markConnected();
+        });
+
+        mNetwork = mNetworkAgent.getNetwork();
+        mNetworkCallback.expectAvailableThenValidatedCallbacks(mNetwork, 5_000);
+        mSocket = new DatagramSocket(TEST_UDP_PORT);
+        mSocket.setSoTimeout(1_000);
+        mNetwork.bindSocket(mSocket);
+    }
+
+    @After
+    public void tearDown() throws IOException {
+        if (mContext != null) {
+            // whatever happens, don't leave the device in rate limited state.
+            ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext, -1);
+        }
+        if (mSocket != null) mSocket.close();
+        if (mNetworkAgent != null) mNetworkAgent.unregister();
+        if (mTunInterface != null) mTunInterface.getFileDescriptor().close();
+        if (mCm != null) mCm.unregisterNetworkCallback(mNetworkCallback);
+    }
+
+    private void assertGreaterThan(final String msg, long lhs, long rhs) {
+        assertTrue(msg + " -- Failed comparison: " + lhs + " > " + rhs, lhs > rhs);
+    }
+
+    private void assertLessThan(final String msg, long lhs, long rhs) {
+        assertTrue(msg + " -- Failed comparison: " + lhs + " < " + rhs, lhs < rhs);
+    }
+
+    private static void sendPacketsToTunInterfaceForDuration(final TestNetworkInterface iface,
+            final Duration duration) throws Exception {
+        final ByteBuffer buffer = PacketBuilder.allocate(false, IPPROTO_IP, IPPROTO_UDP,
+                PAYLOAD.length);
+        final PacketBuilder builder = new PacketBuilder(buffer);
+        builder.writeIpv4Header(TOS, ID, DONT_FRAG_FLAG_MASK, TIME_TO_LIVE,
+                (byte) IPPROTO_UDP, (Inet4Address) REMOTE_IP4_ADDR,
+                (Inet4Address) LOCAL_IP4_ADDR.getAddress());
+        builder.writeUdpHeader((short) TEST_UDP_PORT, (short) TEST_UDP_PORT);
+        buffer.put(PAYLOAD);
+        builder.finalizePacket();
+
+        // write packets to the tun fd as fast as possible for duration.
+        long endMillis = SystemClock.elapsedRealtime() + duration.toMillis();
+        while (SystemClock.elapsedRealtime() < endMillis) {
+            Os.write(iface.getFileDescriptor().getFileDescriptor(), buffer.array(), 0,
+                    buffer.limit());
+        }
+    }
+
+    private static class RateMeasurementSocketReader extends Thread {
+        private volatile boolean mIsRunning = false;
+        private DatagramSocket mSocket;
+        private long mStartMillis = 0;
+        private long mStopMillis = 0;
+        private long mBytesReceived = 0;
+
+        RateMeasurementSocketReader(DatagramSocket socket) throws Exception {
+            mSocket = socket;
+        }
+
+        public void startTest() {
+            mIsRunning = true;
+            start();
+        }
+
+        public long stopAndGetResult() throws Exception {
+            mIsRunning = false;
+            join();
+
+            final long durationMillis = mStopMillis - mStartMillis;
+            return (long) ((double) mBytesReceived / (durationMillis / 1000.0));
+        }
+
+        @Override
+        public void run() {
+            // Since the actual data is not used, the buffer can just be recycled.
+            final byte[] recvBuf = new byte[ETHER_MTU];
+            final DatagramPacket receivedPacket = new DatagramPacket(recvBuf, recvBuf.length);
+            while (mIsRunning) {
+                try {
+                    mSocket.receive(receivedPacket);
+
+                    // don't start the test until after the first packet is received and increment
+                    // mBytesReceived starting with the second packet.
+                    long time = SystemClock.elapsedRealtime();
+                    if (mStartMillis == 0) {
+                        mStartMillis = time;
+                    } else {
+                        mBytesReceived += receivedPacket.getLength();
+                    }
+                    // there may not be another packet, update the stop time on every iteration.
+                    mStopMillis = time;
+                } catch (SocketTimeoutException e) {
+                    // sender has stopped sending data, do nothing and return.
+                } catch (IOException e) {
+                    Log.e(TAG, "socket receive failed", e);
+                }
+            }
+        }
+    }
+
+    private long runIngressDataRateMeasurement(final Duration testDuration) throws Exception {
+        final RateMeasurementSocketReader reader = new RateMeasurementSocketReader(mSocket);
+        reader.startTest();
+        sendPacketsToTunInterfaceForDuration(mTunInterface, testDuration);
+        return reader.stopAndGetResult();
+    }
+
+    void waitForTcPoliceFilterInstalled(Duration timeout) throws IOException {
+        final String command = MessageFormat.format("tc filter show ingress dev {0}",
+                mTunInterface.getInterfaceName());
+        // wait for tc police to show up
+        final long startTime = SystemClock.elapsedRealtime();
+        final long timeoutTime = startTime + timeout.toMillis();
+        while (!SystemUtil.runShellCommand(command).contains("police")) {
+            assertLessThan("timed out waiting for tc police filter",
+                    SystemClock.elapsedRealtime(), timeoutTime);
+            SystemClock.sleep(10);
+        }
+        Log.v(TAG, "waited " + (SystemClock.elapsedRealtime() - startTime)
+                + "ms for tc police filter to appear");
+    }
+
+    @Test
+    public void testIngressRateLimit_testLimit() throws Exception {
+        assumeKernelSupport();
+
+        // If this value is too low, this test might become flaky because of the burst value that
+        // allows to send at a higher data rate for a short period of time. The faster the data rate
+        // and the longer the test, the less this test will be affected.
+        final long dataLimitInBytesPerSecond = 1_000_000; // 1MB/s
+        long resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(1));
+        assertGreaterThan("Failed initial test with rate limit disabled", resultInBytesPerSecond,
+                dataLimitInBytesPerSecond);
+
+        // enable rate limit and wait until the tc filter is installed before starting the test.
+        ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext,
+                dataLimitInBytesPerSecond);
+        waitForTcPoliceFilterInstalled(Duration.ofSeconds(1));
+
+        resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(10));
+        // Add 1% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
+        assertLessThan("Failed test with rate limit enabled", resultInBytesPerSecond,
+                (long) (dataLimitInBytesPerSecond * 1.01));
+
+        ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext, -1);
+
+        resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(1));
+        assertGreaterThan("Failed test with rate limit disabled", resultInBytesPerSecond,
+                dataLimitInBytesPerSecond);
+    }
+
+    @Test
+    public void testIngressRateLimit_testSetting() {
+        int dataLimitInBytesPerSecond = 1_000_000;
+        ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext,
+                dataLimitInBytesPerSecond);
+        assertEquals(dataLimitInBytesPerSecond,
+                ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(mContext));
+        ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext, -1);
+        assertEquals(-1,
+                ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(mContext));
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/TestUtils.java b/tests/cts/net/src/android/net/cts/TestUtils.java
index c1100b1..001aa01 100644
--- a/tests/cts/net/src/android/net/cts/TestUtils.java
+++ b/tests/cts/net/src/android/net/cts/TestUtils.java
@@ -16,6 +16,8 @@
 
 package android.net.cts;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
 import android.os.Build;
 
 import com.android.modules.utils.build.SdkLevel;
@@ -33,4 +35,13 @@
     public static boolean shouldTestSApis() {
         return SdkLevel.isAtLeastS() && ConstantsShim.VERSION > Build.VERSION_CODES.R;
     }
+
+    /**
+     * Whether to test T+ APIs. This requires a) that the test be running on an S+ device, and
+     * b) that the code be compiled against shims new enough to access these APIs.
+     */
+    public static boolean shouldTestTApis() {
+        // TODO: replace SC_V2 with Build.VERSION_CODES.S_V2 when it's available in mainline branch.
+        return SdkLevel.isAtLeastT() && ConstantsShim.VERSION > SC_V2;
+    }
 }
diff --git a/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
new file mode 100644
index 0000000..b4ebcdb
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts.util;
+
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_128;
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_UNUSED;
+
+import android.net.ipsec.ike.ChildSaProposal;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeSaProposal;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.SaProposal;
+import android.net.ipsec.ike.TunnelModeChildSessionParams;
+
+/** Shared testing parameters and util methods for testing IKE */
+public class IkeSessionTestUtils {
+    private static final String TEST_CLIENT_ADDR = "test.client.com";
+    private static final String TEST_SERVER_ADDR = "test.server.com";
+    private static final String TEST_SERVER = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
+
+    public static final IkeSaProposal SA_PROPOSAL = new IkeSaProposal.Builder()
+            .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES, KEY_LEN_UNUSED)
+            .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
+            .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
+            .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
+            .build();
+    public static final ChildSaProposal CHILD_PROPOSAL = new ChildSaProposal.Builder()
+            .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128)
+            .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE)
+            .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
+            .build();
+
+    public static final IkeSessionParams IKE_PARAMS =
+            new IkeSessionParams.Builder()
+                    .setServerHostname(TEST_SERVER)
+                    .addSaProposal(SA_PROPOSAL)
+                    .setLocalIdentification(new IkeFqdnIdentification(TEST_CLIENT_ADDR))
+                    .setRemoteIdentification(new IkeFqdnIdentification(TEST_SERVER_ADDR))
+                    .setAuthPsk("psk".getBytes())
+                    .build();
+    public static final TunnelModeChildSessionParams CHILD_PARAMS =
+            new TunnelModeChildSessionParams.Builder()
+                    .addSaProposal(CHILD_PROPOSAL)
+                    .build();
+}
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
new file mode 100644
index 0000000..37ad7cb
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsNetTestCasesInternetPermission",
+    defaults: ["cts_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: ["ctstestrunner-axt"],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+}
diff --git a/tests/cts/netpermission/internetpermission/AndroidManifest.xml b/tests/cts/netpermission/internetpermission/AndroidManifest.xml
new file mode 100644
index 0000000..45ef5bd
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/AndroidManifest.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.networkpermission.internetpermission.cts">
+
+    <application>
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.networkpermission.internetpermission.cts.InternetPermissionTest"
+             android:label="InternetPermissionTest"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <!--
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
+
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.networkpermission.internetpermission.cts"
+         android:label="CTS tests for INTERNET permissions">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+
+</manifest>
diff --git a/tests/cts/netpermission/internetpermission/AndroidTest.xml b/tests/cts/netpermission/internetpermission/AndroidTest.xml
new file mode 100644
index 0000000..3b23e72
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS internet permission test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="not-shardable" value="true" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsNetTestCasesInternetPermission.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.networkpermission.internetpermission.cts" />
+        <option name="runtime-hint" value="10s" />
+    </test>
+</configuration>
diff --git a/tests/cts/netpermission/internetpermission/TEST_MAPPING b/tests/cts/netpermission/internetpermission/TEST_MAPPING
new file mode 100644
index 0000000..60877f4
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetTestCasesInternetPermission"
+    }
+  ]
+}
diff --git a/tests/cts/netpermission/internetpermission/src/android/net/cts/network/permission/InternetPermissionTest.java b/tests/cts/netpermission/internetpermission/src/android/net/cts/network/permission/InternetPermissionTest.java
new file mode 100644
index 0000000..2b7c8b5
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/src/android/net/cts/network/permission/InternetPermissionTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts.networkpermission.internetpermission;
+
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Socket;
+/**
+* Test that protected android.net.ConnectivityManager methods cannot be called without
+* permissions
+*/
+@RunWith(AndroidJUnit4.class)
+public class InternetPermissionTest {
+
+    /**
+     * Verify that create inet socket failed because of the permission is missing.
+     * <p>Tests Permission:
+     *   {@link android.Manifest.permission#INTERNET}.
+     */
+    @SmallTest
+    @Test
+    public void testCreateSocket() throws Exception {
+        try {
+            Socket socket = new Socket("example.com", 80);
+            fail("Ceate inet socket did not throw SecurityException as expected");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+}
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
new file mode 100644
index 0000000..7a24886
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsNetTestCasesUpdateStatsPermission",
+    defaults: ["cts_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: ["ctstestrunner-axt"],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+}
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidManifest.xml b/tests/cts/netpermission/updatestatspermission/AndroidManifest.xml
new file mode 100644
index 0000000..6babe8f
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/AndroidManifest.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.networkpermission.updatestatspermission.cts">
+
+    <!--
+                 This CTS test is designed to test that an unprivileged app cannot get the
+                 UPDATE_DEVICE_STATS permission even if it specified it in the manifest. the
+                 UPDATE_DEVICE_STATS permission is a signature|privileged permission that CTS
+                 test cannot have.
+            -->
+    <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <application>
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.networkpermission.updatestatspermission.cts.UpdateStatsPermissionTest"
+             android:label="UpdateStatsPermissionTest"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <!--
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
+
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.networkpermission.updatestatspermission.cts"
+         android:label="CTS tests for UPDATE_DEVICE_STATS permissions">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+
+</manifest>
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
new file mode 100644
index 0000000..c47cad9
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS update stats permission test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="not-shardable" value="true" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsNetTestCasesUpdateStatsPermission.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.networkpermission.updatestatspermission.cts" />
+        <option name="runtime-hint" value="10s" />
+    </test>
+</configuration>
diff --git a/tests/cts/netpermission/updatestatspermission/TEST_MAPPING b/tests/cts/netpermission/updatestatspermission/TEST_MAPPING
new file mode 100644
index 0000000..6d6dfe0
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetTestCasesUpdateStatsPermission"
+    }
+  ]
+}
diff --git a/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
new file mode 100644
index 0000000..bea843c
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts.networkpermission.updatestatspermission;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.TrafficStats;
+import android.os.Process;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+* Test that protected android.net.ConnectivityManager methods cannot be called without
+* permissions
+*/
+@RunWith(AndroidJUnit4.class)
+public class UpdateStatsPermissionTest {
+
+    /**
+     * Verify that setCounterSet for a different uid failed because of the permission cannot be
+     * granted to a third-party app.
+     * <p>Tests Permission:
+     *   {@link android.Manifest.permission#UPDATE_DEVICE_STATS}.
+     */
+    @SmallTest
+    @Test
+    public void testUpdateDeviceStatsPermission() throws Exception {
+
+        // Set the current thread uid to a another uid. It should silently fail when tagging the
+        // socket since the current process doesn't have UPDATE_DEVICE_STATS permission.
+        TrafficStats.setThreadStatsTag(0);
+        TrafficStats.setThreadStatsUid(/*root uid*/ 0);
+        Socket socket = new Socket("example.com", 80);
+        TrafficStats.tagSocket(socket);
+
+        // Transfer 1K of data to a remote host and verify the stats is still billed to the current
+        // uid.
+        final int byteCount = 1024;
+
+        socket.setTcpNoDelay(true);
+        socket.setSoLinger(true, 0);
+        OutputStream out = socket.getOutputStream();
+        byte[] buf = new byte[byteCount];
+        final long uidTxBytesBefore = TrafficStats.getUidTxBytes(Process.myUid());
+        out.write(buf);
+        out.close();
+        socket.close();
+        long uidTxBytesAfter = TrafficStats.getUidTxBytes(Process.myUid());
+        long uidTxDeltaBytes = uidTxBytesAfter - uidTxBytesBefore;
+        assertTrue("uidtxb: " + uidTxBytesBefore + " -> " + uidTxBytesAfter + " delta="
+                + uidTxDeltaBytes + " >= " + byteCount, uidTxDeltaBytes >= byteCount);
+    }
+
+    static final int UNSUPPORTED = -1;
+
+    /**
+     * Verify that get TrafficStats of a different uid failed because of the permission is not
+     * granted to a third-party app.
+     * <p>Tests Permission:
+     *   {@link android.Manifest.permission#UPDATE_DEVICE_STATS}.
+     */
+    @SmallTest
+    @Test
+    public void testGetStatsOfOtherUid() throws Exception {
+        // Test get stats of another uid failed since the current process does not have permission
+        assertEquals(UNSUPPORTED, TrafficStats.getUidRxBytes(/*root uid*/ 0));
+    }
+}
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 530fa91..97c1265 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -40,7 +40,7 @@
         "mockito-target-extended-minus-junit4",
         "net-tests-utils",
         "service-connectivity-pre-jarjar",
-        "services.core",
+        "service-connectivity-tiramisu-pre-jarjar",
         "services.net",
         "testables",
     ],
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index 365c0cf..2763f5a 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -343,6 +343,10 @@
         return mNetworkAgent;
     }
 
+    public NetworkAgentConfig getNetworkAgentConfig() {
+        return mNetworkAgentConfig;
+    }
+
     public NetworkCapabilities getNetworkCapabilities() {
         return mNetworkCapabilities;
     }
diff --git a/tests/native/Android.bp b/tests/native/Android.bp
new file mode 100644
index 0000000..a8d908a
--- /dev/null
+++ b/tests/native/Android.bp
@@ -0,0 +1,33 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+    name: "connectivity_native_test",
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+        "vts",
+    ],
+    test_config_template: "AndroidTestTemplate.xml",
+    min_sdk_version: "31",
+    tidy: false,
+    srcs: [
+        "connectivity_native_test.cpp",
+    ],
+    header_libs: ["bpf_connectivity_headers"],
+    shared_libs: [
+        "libbase",
+        "libbinder_ndk",
+        "liblog",
+        "libnetutils",
+        "libprocessgroup",
+    ],
+    static_libs: [
+        "connectivity_native_aidl_interface-lateststable-ndk",
+        "libcutils",
+        "libmodules-utils-build",
+        "libutils",
+    ],
+    compile_multilib: "first",
+}
diff --git a/tests/native/AndroidTestTemplate.xml b/tests/native/AndroidTestTemplate.xml
new file mode 100644
index 0000000..44e35a9
--- /dev/null
+++ b/tests/native/AndroidTestTemplate.xml
@@ -0,0 +1,30 @@
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Configuration for connectivity {MODULE} tests">
+    <option name="test-suite-tag" value="mts" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+    <!-- The tested code is only part of a SDK 30+ module (Tethering) -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="{MODULE}" />
+    </test>
+</configuration>
diff --git a/tests/native/OWNERS b/tests/native/OWNERS
new file mode 100644
index 0000000..8dfa455
--- /dev/null
+++ b/tests/native/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 31808
+set noparent
+file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking_xts
diff --git a/tests/native/connectivity_native_test.cpp b/tests/native/connectivity_native_test.cpp
new file mode 100644
index 0000000..8b089ab
--- /dev/null
+++ b/tests/native/connectivity_native_test.cpp
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <android-modules-utils/sdk_level.h>
+#include <cutils/misc.h>  // FIRST_APPLICATION_UID
+#include <gtest/gtest.h>
+#include <netinet/in.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+
+#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
+
+using aidl::android::net::connectivity::aidl::IConnectivityNative;
+
+class ConnectivityNativeBinderTest : public ::testing::Test {
+  public:
+    std::vector<int32_t> mActualBlockedPorts;
+
+    ConnectivityNativeBinderTest() {
+        AIBinder* binder = AServiceManager_getService("connectivity_native");
+        ndk::SpAIBinder sBinder = ndk::SpAIBinder(binder);
+        mService = aidl::android::net::connectivity::aidl::IConnectivityNative::fromBinder(sBinder);
+    }
+
+    void SetUp() override {
+        // Skip test case if not on T.
+        if (!android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() <<
+                "Should be at least T device.";
+
+        ASSERT_NE(nullptr, mService.get());
+
+        // If there are already ports being blocked on device unblockAllPortsForBind() store
+        // the currently blocked ports and add them back at the end of the test. Do this for
+        // every test case so additional test cases do not forget to add ports back.
+        ndk::ScopedAStatus status = mService->getPortsBlockedForBind(&mActualBlockedPorts);
+        EXPECT_TRUE(status.isOk()) << status.getDescription ();
+
+    }
+
+    void TearDown() override {
+        ndk::ScopedAStatus status;
+        if (mActualBlockedPorts.size() > 0) {
+            for (int i : mActualBlockedPorts) {
+                mService->blockPortForBind(i);
+                EXPECT_TRUE(status.isOk()) << status.getDescription ();
+            }
+        }
+    }
+
+  protected:
+    std::shared_ptr<IConnectivityNative> mService;
+
+    void runSocketTest (sa_family_t family, const int type, bool blockPort) {
+        ndk::ScopedAStatus status;
+        in_port_t port = 0;
+        int sock, sock2;
+        // Open two sockets with SO_REUSEADDR and expect they can both bind to port.
+        sock = openSocket(&port, family, type, false /* expectBindFail */);
+        sock2 = openSocket(&port, family, type, false /* expectBindFail */);
+
+        int blockedPort = 0;
+        if (blockPort) {
+            blockedPort = ntohs(port);
+            status = mService->blockPortForBind(blockedPort);
+            EXPECT_TRUE(status.isOk()) << status.getDescription ();
+        }
+
+        int sock3 = openSocket(&port, family, type, blockPort /* expectBindFail */);
+
+        if (blockPort) {
+            EXPECT_EQ(-1, sock3);
+            status = mService->unblockPortForBind(blockedPort);
+            EXPECT_TRUE(status.isOk()) << status.getDescription ();
+        } else {
+            EXPECT_NE(-1, sock3);
+        }
+
+        close(sock);
+        close(sock2);
+        close(sock3);
+    }
+
+    /*
+    * Open the socket and update the port.
+    */
+    int openSocket(in_port_t* port, sa_family_t family, const int type, bool expectBindFail) {
+        int ret = 0;
+        int enable = 1;
+        const int sock = socket(family, type, 0);
+        ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable));
+        EXPECT_EQ(0, ret);
+
+        if (family == AF_INET) {
+            struct sockaddr_in addr4 = { .sin_family = family, .sin_port = htons(*port) };
+            ret = bind(sock, (struct sockaddr*) &addr4, sizeof(addr4));
+        } else {
+            struct sockaddr_in6 addr6 = { .sin6_family = family, .sin6_port = htons(*port) };
+            ret = bind(sock, (struct sockaddr*) &addr6, sizeof(addr6));
+        }
+
+        if (expectBindFail) {
+            EXPECT_NE(0, ret);
+            // If port is blocked, return here since the port is not needed
+            // for subsequent sockets.
+            close(sock);
+            return -1;
+        }
+        EXPECT_EQ(0, ret) << "bind unexpectedly failed, errno: " << errno;
+
+        if (family == AF_INET) {
+            struct sockaddr_in sin;
+            socklen_t len = sizeof(sin);
+            EXPECT_NE(-1, getsockname(sock, (struct sockaddr *)&sin, &len));
+            EXPECT_NE(0, ntohs(sin.sin_port));
+            if (*port != 0) EXPECT_EQ(*port, ntohs(sin.sin_port));
+            *port = ntohs(sin.sin_port);
+        } else {
+            struct sockaddr_in6 sin;
+            socklen_t len = sizeof(sin);
+            EXPECT_NE(-1, getsockname(sock, (struct sockaddr *)&sin, &len));
+            EXPECT_NE(0, ntohs(sin.sin6_port));
+            if (*port != 0) EXPECT_EQ(*port, ntohs(sin.sin6_port));
+            *port = ntohs(sin.sin6_port);
+        }
+        return sock;
+    }
+};
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV4Udp) {
+    runSocketTest(AF_INET, SOCK_DGRAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV4Tcp) {
+    runSocketTest(AF_INET, SOCK_STREAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV6Udp) {
+    runSocketTest(AF_INET6, SOCK_DGRAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV6Tcp) {
+    runSocketTest(AF_INET6, SOCK_STREAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort4Udp) {
+    runSocketTest(AF_INET, SOCK_DGRAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort4Tcp) {
+    runSocketTest(AF_INET, SOCK_STREAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort6Udp) {
+    runSocketTest(AF_INET6, SOCK_DGRAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort6Tcp) {
+    runSocketTest(AF_INET6, SOCK_STREAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) {
+    ndk::ScopedAStatus status = mService->blockPortForBind(5555);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    status = mService->blockPortForBind(5555);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    status = mService->unblockPortForBind(5555);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+}
+
+TEST_F(ConnectivityNativeBinderTest, GetBlockedPorts) {
+    ndk::ScopedAStatus status;
+    std::vector<int> blockedPorts{1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
+    for (int i : blockedPorts) {
+        status = mService->blockPortForBind(i);
+        EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    }
+    std::vector<int32_t> actualBlockedPorts;
+    status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    EXPECT_FALSE(actualBlockedPorts.empty());
+    EXPECT_EQ(blockedPorts, actualBlockedPorts);
+
+    // Remove the ports we added.
+    status = mService->unblockAllPortsForBind();
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    EXPECT_TRUE(actualBlockedPorts.empty());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockAllPorts) {
+    ndk::ScopedAStatus status;
+    std::vector<int> blockedPorts{1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
+
+    if (mActualBlockedPorts.size() > 0) {
+        status = mService->unblockAllPortsForBind();
+    }
+
+    for (int i : blockedPorts) {
+        status = mService->blockPortForBind(i);
+        EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    }
+
+    std::vector<int32_t> actualBlockedPorts;
+    status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    EXPECT_FALSE(actualBlockedPorts.empty());
+
+    status = mService->unblockAllPortsForBind();
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    EXPECT_TRUE(actualBlockedPorts.empty());
+    // If mActualBlockedPorts is not empty, ports will be added back in teardown.
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockNegativePort) {
+    int retry = 0;
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->blockPortForBind(-1);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockNegativePort) {
+    int retry = 0;
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->unblockPortForBind(-1);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockMaxPort) {
+    int retry = 0;
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->blockPortForBind(65536);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockMaxPort) {
+    int retry = 0;
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->unblockPortForBind(65536);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
+    int retry = 0;
+    int curUid = getuid();
+    EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->blockPortForBind(5555);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_SECURITY, status.getExceptionCode());
+    EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
+}
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
index 8011540..df8ab74 100644
--- a/tests/smoketest/Android.bp
+++ b/tests/smoketest/Android.bp
@@ -22,6 +22,6 @@
     static_libs: [
         "androidx.test.rules",
         "mockito-target-minus-junit4",
-        "services.core",
+        "service-connectivity",
     ],
 }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index fde7bac..5b926de 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -3,10 +3,6 @@
 //########################################################################
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "Android-Apache-2.0"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
@@ -28,7 +24,6 @@
         "libbinder",
         "libbpf_bcc",
         "libc++",
-        "libcgrouprc",
         "libcrypto",
         "libcutils",
         "libdl_android",
@@ -43,7 +38,6 @@
         "libnetworkstatsfactorytestjni",
         "libpackagelistparser",
         "libpcre2",
-        "libprocessgroup",
         "libselinux",
         "libtinyxml2",
         "libui",
@@ -61,6 +55,7 @@
     name: "non-connectivity-module-test",
     srcs: [
         "java/android/app/usage/*.java",
+        "java/android/net/EthernetNetworkUpdateRequestTest.java",
         "java/android/net/Ikev2VpnProfileTest.java",
         "java/android/net/IpMemoryStoreTest.java",
         "java/android/net/IpSecAlgorithmTest.java",
@@ -68,6 +63,7 @@
         "java/android/net/IpSecManagerTest.java",
         "java/android/net/IpSecTransformTest.java",
         "java/android/net/KeepalivePacketDataUtilTest.java",
+        "java/android/net/NetworkIdentitySetTest.kt",
         "java/android/net/NetworkIdentityTest.kt",
         "java/android/net/NetworkStats*.java",
         "java/android/net/NetworkTemplateTest.kt",
@@ -88,8 +84,10 @@
         "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
         "java/com/android/server/connectivity/VpnTest.java",
+        "java/com/android/server/ethernet/*.java",
         "java/com/android/server/net/ipmemorystore/*.java",
         "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
+        "java/com/android/server/net/IpConfigStoreTest.java",
         "java/com/android/server/net/NetworkStats*.java",
         "java/com/android/server/net/TestableUsageCallback.kt",
     ]
@@ -133,6 +131,7 @@
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
         "services.core-vpn",
+        "cts-net-utils"
     ],
     libs: [
         "android.net.ipsec.ike.stubs.module_lib",
@@ -167,6 +166,7 @@
         "services.net",
     ],
     jni_libs: [
+        "libandroid_net_connectivity_com_android_net_module_util_jni",
         "libservice-connectivity",
         "libandroid_net_connectivity_com_android_net_module_util_jni",
     ],
diff --git a/tests/unit/java/android/net/EthernetNetworkUpdateRequestTest.java b/tests/unit/java/android/net/EthernetNetworkUpdateRequestTest.java
new file mode 100644
index 0000000..ca9558b
--- /dev/null
+++ b/tests/unit/java/android/net/EthernetNetworkUpdateRequestTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class EthernetNetworkUpdateRequestTest {
+    private IpConfiguration buildIpConfiguration() {
+        return new IpConfiguration.Builder().setHttpProxy(
+                new ProxyInfo("test.example.com", 1234, "")).build();
+    }
+
+    private NetworkCapabilities buildNetworkCapabilities() {
+        return new NetworkCapabilities.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_ETHERNET).build();
+    }
+
+    @Test
+    public void testParcelUnparcel() {
+        EthernetNetworkUpdateRequest reqWithNonNull =
+                new EthernetNetworkUpdateRequest.Builder().setIpConfiguration(
+                        buildIpConfiguration()).setNetworkCapabilities(
+                        buildNetworkCapabilities()).build();
+        EthernetNetworkUpdateRequest reqWithNullCaps =
+                new EthernetNetworkUpdateRequest.Builder().setIpConfiguration(
+                        buildIpConfiguration()).build();
+        EthernetNetworkUpdateRequest reqWithNullConfig =
+                new EthernetNetworkUpdateRequest.Builder().setNetworkCapabilities(
+                        buildNetworkCapabilities()).build();
+
+        assertParcelingIsLossless(reqWithNonNull);
+        assertParcelingIsLossless(reqWithNullCaps);
+        assertParcelingIsLossless(reqWithNullConfig);
+    }
+
+    @Test
+    public void testEmptyUpdateRequestThrows() {
+        EthernetNetworkUpdateRequest.Builder emptyBuilder =
+                new EthernetNetworkUpdateRequest.Builder();
+        assertThrows(IllegalStateException.class, () -> emptyBuilder.build());
+    }
+}
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
index 8559c20..8222ca1 100644
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -16,6 +16,9 @@
 
 package android.net;
 
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
+
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -25,6 +28,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.os.Build;
 import android.test.mock.MockContext;
 
@@ -441,6 +445,33 @@
         assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
     }
 
+    @Test
+    public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
+        final IkeTunnelConnectionParams tunnelParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+        // Config authentication related fields is not required while building with
+        // IkeTunnelConnectionParams.
+        final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testEquals() throws Exception {
+        // Verify building without IkeTunnelConnectionParams
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        assertEquals(builder.build(), builder.build());
+
+        // Verify building with IkeTunnelConnectionParams
+        final IkeTunnelConnectionParams tunnelParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+        final IkeTunnelConnectionParams tunnelParams2 =
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+        assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
+                new Ikev2VpnProfile.Builder(tunnelParams2).build());
+    }
+
+
     private static class CertificateAndKey {
         public final X509Certificate cert;
         public final PrivateKey key;
diff --git a/tests/unit/java/android/net/NetworkIdentitySetTest.kt b/tests/unit/java/android/net/NetworkIdentitySetTest.kt
new file mode 100644
index 0000000..d61ebf9
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkIdentitySetTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.content.Context
+import android.net.ConnectivityManager.TYPE_MOBILE
+import android.os.Build
+import android.telephony.TelephonyManager
+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
+
+private const val TEST_IMSI1 = "testimsi1"
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkIdentitySetTest {
+    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 testCompare() {
+        val ident1 = NetworkIdentity.buildNetworkIdentity(mockContext,
+            buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
+            false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        val ident2 = NetworkIdentity.buildNetworkIdentity(mockContext,
+            buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
+            true /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+
+        // Verify that the results of comparing two empty sets are equal
+        assertEquals(0, NetworkIdentitySet.compare(NetworkIdentitySet(), NetworkIdentitySet()))
+
+        val identSet1 = NetworkIdentitySet()
+        val identSet2 = NetworkIdentitySet()
+        identSet1.add(ident1)
+        identSet2.add(ident2)
+        assertEquals(-1, NetworkIdentitySet.compare(NetworkIdentitySet(), identSet1))
+        assertEquals(1, NetworkIdentitySet.compare(identSet1, NetworkIdentitySet()))
+        assertEquals(0, NetworkIdentitySet.compare(identSet1, identSet1))
+        assertEquals(-1, NetworkIdentitySet.compare(identSet1, identSet2))
+    }
+}
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index 4b2d874..bf5568d 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -22,6 +22,7 @@
 import android.net.ConnectivityManager.TYPE_MOBILE
 import android.net.ConnectivityManager.TYPE_NONE
 import android.net.ConnectivityManager.TYPE_WIFI
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkIdentity.OEM_NONE
 import android.net.NetworkIdentity.OEM_PAID
 import android.net.NetworkIdentity.OEM_PRIVATE
@@ -39,8 +40,11 @@
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
-private const val TEST_IMSI = "testimsi"
 private const val TEST_WIFI_KEY = "testwifikey"
+private const val TEST_IMSI1 = "testimsi1"
+private const val TEST_IMSI2 = "testimsi2"
+private const val TEST_SUBID1 = 1
+private const val TEST_SUBID2 = 2
 
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
@@ -84,7 +88,7 @@
     fun testIsMetered() {
         // Verify network is metered.
         val netIdent1 = NetworkIdentity.buildNetworkIdentity(mockContext,
-                buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI),
+                buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
                 false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
         assertTrue(netIdent1.isMetered())
 
@@ -93,7 +97,7 @@
             addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
         }.build()
         val netIdent2 = NetworkIdentity.buildNetworkIdentity(mockContext,
-                buildMobileNetworkStateSnapshot(capsNotMetered, TEST_IMSI),
+                buildMobileNetworkStateSnapshot(capsNotMetered, TEST_IMSI1),
                 false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
         assertFalse(netIdent2.isMetered())
 
@@ -103,33 +107,38 @@
             setCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED, true)
         }
         val netIdent3 = NetworkIdentity.buildNetworkIdentity(mockContext,
-                buildMobileNetworkStateSnapshot(capsTempNotMetered, TEST_IMSI),
+                buildMobileNetworkStateSnapshot(capsTempNotMetered, TEST_IMSI1),
                 false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
         assertFalse(netIdent3.isMetered())
     }
 
     @Test
     fun testBuilder() {
+        val specifier1 = TelephonyNetworkSpecifier(TEST_SUBID1)
         val oemPrivateRoamingNotMeteredCap = NetworkCapabilities().apply {
             addCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)
             addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+            addTransportType(TRANSPORT_CELLULAR)
+            setNetworkSpecifier(specifier1)
         }
         val identFromSnapshot = NetworkIdentity.Builder().setNetworkStateSnapshot(
-                buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI))
+                buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI1))
                 .setDefaultNetwork(true)
                 .setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
+                .setSubId(TEST_SUBID1)
                 .build()
         val identFromLegacyBuild = NetworkIdentity.buildNetworkIdentity(mockContext,
-                buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI),
+                buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI1),
                 true /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
         val identFromConstructor = NetworkIdentity(TYPE_MOBILE,
                 TelephonyManager.NETWORK_TYPE_UMTS,
-                TEST_IMSI,
+                TEST_IMSI1,
                 null /* wifiNetworkKey */,
                 true /* roaming */,
                 false /* metered */,
                 true /* defaultNetwork */,
-                NetworkTemplate.OEM_MANAGED_PRIVATE)
+                NetworkTemplate.OEM_MANAGED_PRIVATE,
+                TEST_SUBID1)
         assertEquals(identFromLegacyBuild, identFromSnapshot)
         assertEquals(identFromConstructor, identFromSnapshot)
 
@@ -222,4 +231,28 @@
                             }
                 }
     }
+
+    @Test
+    fun testGetSubId() {
+        val specifier1 = TelephonyNetworkSpecifier(TEST_SUBID1)
+        val specifier2 = TelephonyNetworkSpecifier(TEST_SUBID2)
+        val capSUBID1 = NetworkCapabilities().apply {
+            addTransportType(TRANSPORT_CELLULAR)
+            setNetworkSpecifier(specifier1)
+        }
+        val capSUBID2 = NetworkCapabilities().apply {
+            addTransportType(TRANSPORT_CELLULAR)
+            setNetworkSpecifier(specifier2)
+        }
+
+        val netIdent1 = NetworkIdentity.buildNetworkIdentity(mockContext,
+                buildMobileNetworkStateSnapshot(capSUBID1, TEST_IMSI1),
+                false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        assertEquals(TEST_SUBID1, netIdent1.getSubId())
+
+        val netIdent2 = NetworkIdentity.buildNetworkIdentity(mockContext,
+                buildMobileNetworkStateSnapshot(capSUBID2, TEST_IMSI2),
+                false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        assertEquals(TEST_SUBID2, netIdent2.getSubId())
+    }
 }
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index c27ee93..32c106d 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -87,6 +87,7 @@
 
     private static final String TEST_FILE = "test.bin";
     private static final String TEST_IMSI = "310260000000000";
+    private static final int TEST_SUBID = 1;
 
     private static final long TIME_A = 1326088800000L; // UTC: Monday 9th January 2012 06:00:00 AM
     private static final long TIME_B = 1326110400000L; // UTC: Monday 9th January 2012 12:00:00 PM
@@ -213,7 +214,7 @@
         final NetworkStats.Entry entry = new NetworkStats.Entry();
         final NetworkIdentitySet identSet = new NetworkIdentitySet();
         identSet.add(new NetworkIdentity(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN,
-                TEST_IMSI, null, false, true, true, OEM_NONE));
+                TEST_IMSI, null, false, true, true, OEM_NONE, TEST_SUBID));
 
         int myUid = Process.myUid();
         int otherUidInSameUser = Process.myUid() + 1;
@@ -475,7 +476,7 @@
         final NetworkStatsCollection large = new NetworkStatsCollection(HOUR_IN_MILLIS);
         final NetworkIdentitySet ident = new NetworkIdentitySet();
         ident.add(new NetworkIdentity(ConnectivityManager.TYPE_MOBILE, -1, TEST_IMSI, null,
-                false, true, true, OEM_NONE));
+                false, true, true, OEM_NONE, TEST_SUBID));
         large.recordData(ident, UID_ALL, SET_ALL, TAG_NONE, TIME_A, TIME_B,
                 new NetworkStats.Entry(12_730_893_164L, 1, 0, 0, 0));
 
diff --git a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
index e4943ea..743d39e 100644
--- a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
+++ b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
@@ -46,7 +46,7 @@
 
     @Test
     fun testReadPlatformCollection() {
-        // Verify the method throws for wrong file version.
+        // Verify the method throws for wrong file format.
         assertFailsWith<ProtocolException> {
             NetworkStatsDataMigrationUtils.readPlatformCollection(
                     NetworkStatsCollection.Builder(BUCKET_DURATION_MS),
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index 943a559..360390d 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -16,6 +16,9 @@
 
 package com.android.internal.net;
 
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
+
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.ParcelUtils.assertParcelSane;
 
@@ -26,6 +29,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.net.IpSecAlgorithm;
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.os.Build;
 
 import androidx.test.filters.SmallTest;
@@ -85,7 +89,8 @@
 
     private VpnProfile getSampleIkev2Profile(String key) {
         final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
-                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */);
+                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
+                null /* ikeTunConnParams */);
 
         p.name = "foo";
         p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
@@ -120,6 +125,35 @@
         return p;
     }
 
+    private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
+        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
+                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS));
+
+        p.name = "foo";
+        p.server = "bar";
+        p.dnsServers = "8.8.8.8";
+        p.searchDomains = "";
+        p.routes = "0.0.0.0/0";
+        p.mppe = false;
+        p.proxy = null;
+        p.setAllowedAlgorithms(
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.CRYPT_AES_CBC));
+        p.isBypassable = true;
+        p.isMetered = true;
+        p.maxMtu = 1350;
+        p.areAuthParamsInline = true;
+
+        // Not saved, but also not compared.
+        p.saveLogin = true;
+
+        return p;
+    }
+
     @Test
     public void testEquals() {
         assertEquals(
@@ -134,13 +168,21 @@
     public void testParcelUnparcel() {
         if (isAtLeastT()) {
             // excludeLocalRoutes, requiresPlatformValidation were added in T.
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 25);
+            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 26);
+            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 26);
         } else {
             assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
         }
     }
 
     @Test
+    public void testEncodeDecodeWithIkeTunConnParams() {
+        final VpnProfile profile = getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY);
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertEquals(profile, decoded);
+    }
+
+    @Test
     public void testEncodeDecode() {
         final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
         final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index ac21e77..f07a10d 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server;
 
-import static android.net.INetd.FIREWALL_CHAIN_DOZABLE;
-import static android.net.INetd.FIREWALL_RULE_ALLOW;
 import static android.net.INetd.PERMISSION_INTERNET;
 
 import static org.junit.Assume.assumeFalse;
@@ -60,26 +58,10 @@
     @Test
     public void testBpfNetMapsBeforeT() throws Exception {
         assumeFalse(SdkLevel.isAtLeastT());
-        mBpfNetMaps.addNaughtyApp(TEST_UID);
-        verify(mNetd).bandwidthAddNaughtyApp(TEST_UID);
-        mBpfNetMaps.removeNaughtyApp(TEST_UID);
-        verify(mNetd).bandwidthRemoveNaughtyApp(TEST_UID);
-        mBpfNetMaps.addNiceApp(TEST_UID);
-        verify(mNetd).bandwidthAddNiceApp(TEST_UID);
-        mBpfNetMaps.removeNiceApp(TEST_UID);
-        verify(mNetd).bandwidthRemoveNiceApp(TEST_UID);
-        mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true);
-        verify(mNetd).firewallEnableChildChain(FIREWALL_CHAIN_DOZABLE, true);
-        mBpfNetMaps.replaceUidChain(CHAINNAME, true, TEST_UIDS);
-        verify(mNetd).firewallReplaceUidChain(CHAINNAME, true, TEST_UIDS);
-        mBpfNetMaps.setUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, FIREWALL_RULE_ALLOW);
-        verify(mNetd).firewallSetUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, FIREWALL_RULE_ALLOW);
         mBpfNetMaps.addUidInterfaceRules(IFNAME, TEST_UIDS);
         verify(mNetd).firewallAddUidInterfaceRules(IFNAME, TEST_UIDS);
         mBpfNetMaps.removeUidInterfaceRules(TEST_UIDS);
         verify(mNetd).firewallRemoveUidInterfaceRules(TEST_UIDS);
-        mBpfNetMaps.swapActiveStatsMap();
-        verify(mNetd).trafficSwapActiveStatsMap();
         mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
         verify(mNetd).trafficSetNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
     }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 16b3d5a..4c76803 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -36,6 +36,7 @@
 import static android.content.Intent.ACTION_USER_REMOVED;
 import static android.content.Intent.ACTION_USER_UNLOCKED;
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
+import static android.content.pm.PackageManager.FEATURE_ETHERNET;
 import static android.content.pm.PackageManager.FEATURE_WIFI;
 import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
@@ -137,6 +138,9 @@
 import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
 import static com.android.testutils.ConcurrentUtils.await;
 import static com.android.testutils.ConcurrentUtils.durationOf;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.ExceptionUtils.ignoreExceptions;
 import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
 import static com.android.testutils.MiscAsserts.assertContainsAll;
@@ -156,6 +160,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
 import static org.mockito.AdditionalMatchers.aryEq;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -279,6 +284,7 @@
 import android.net.shared.NetworkMonitorUtils;
 import android.net.shared.PrivateDnsConfig;
 import android.net.util.MultinetworkPolicyTracker;
+import android.net.wifi.WifiInfo;
 import android.os.BadParcelableException;
 import android.os.BatteryStatsManager;
 import android.os.Binder;
@@ -331,6 +337,7 @@
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.LocationPermissionChecker;
+import com.android.networkstack.apishim.NetworkAgentConfigShimImpl;
 import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
 import com.android.server.ConnectivityService.NetworkRequestInfo;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
@@ -358,6 +365,7 @@
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
@@ -420,6 +428,9 @@
 public class ConnectivityServiceTest {
     private static final String TAG = "ConnectivityServiceTest";
 
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
     private static final int TIMEOUT_MS = 2_000;
     // Broadcasts can take a long time to be delivered. The test will not wait for that long unless
     // there is a failure, so use a long timeout.
@@ -573,6 +584,12 @@
         // is "<permission name>,<pid>,<uid>". PID+UID permissons have priority over generic ones.
         private final HashMap<String, Integer> mMockedPermissions = new HashMap<>();
 
+        private void mockStringResource(int resId) {
+            doAnswer((inv) -> {
+                return "Mock string resource ID=" + inv.getArgument(0);
+            }).when(mInternalResources).getString(resId);
+        }
+
         MockContext(Context base, ContentProvider settingsProvider) {
             super(base);
 
@@ -585,6 +602,18 @@
             }).when(mInternalResources)
                     .getStringArray(com.android.internal.R.array.networkAttributes);
 
+            final int[] stringResourcesToMock = new int[] {
+                com.android.internal.R.string.config_customVpnAlwaysOnDisconnectedDialogComponent,
+                com.android.internal.R.string.vpn_lockdown_config,
+                com.android.internal.R.string.vpn_lockdown_connected,
+                com.android.internal.R.string.vpn_lockdown_connecting,
+                com.android.internal.R.string.vpn_lockdown_disconnected,
+                com.android.internal.R.string.vpn_lockdown_error,
+            };
+            for (int resId : stringResourcesToMock) {
+                mockStringResource(resId);
+            }
+
             mContentResolver = new MockContentResolver();
             mContentResolver.addProvider(Settings.AUTHORITY, settingsProvider);
         }
@@ -907,6 +936,7 @@
                 return null;
             };
 
+            doAnswer(validateAnswer).when(mNetworkMonitor).notifyNetworkConnected(any(), any());
             doAnswer(validateAnswer).when(mNetworkMonitor).notifyNetworkConnectedParcel(any());
             doAnswer(validateAnswer).when(mNetworkMonitor).forceReevaluation(anyInt());
 
@@ -930,6 +960,11 @@
         }
 
         private void onValidationRequested() throws Exception {
+            if (SdkLevel.isAtLeastT()) {
+                verify(mNetworkMonitor).notifyNetworkConnectedParcel(any());
+            } else {
+                verify(mNetworkMonitor).notifyNetworkConnected(any(), any());
+            }
             if (mNmProvNotificationRequested
                     && ((mNmValidationResult & NETWORK_VALIDATION_RESULT_VALID) != 0)) {
                 mNmCallbacks.hideProvisioningNotification();
@@ -1339,6 +1374,10 @@
             return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork();
         }
 
+        public NetworkAgentConfig getNetworkAgentConfig() {
+            return null == mMockNetworkAgent ? null : mMockNetworkAgent.getNetworkAgentConfig();
+        }
+
         @Override
         public int getActiveVpnType() {
             return mVpnType;
@@ -1708,6 +1747,7 @@
         mockDefaultPackages();
         mockHasSystemFeature(FEATURE_WIFI, true);
         mockHasSystemFeature(FEATURE_WIFI_DIRECT, true);
+        mockHasSystemFeature(FEATURE_ETHERNET, true);
         doReturn(true).when(mTelephonyManager).isDataCapable();
 
         FakeSettingsProvider.clearSettingsProvider();
@@ -1974,6 +2014,13 @@
             // updated. Check that this happened.
             assertEquals(-1L, (long) mActiveRateLimit.getOrDefault(iface, -1L));
             mActiveRateLimit.put(iface, rateInBytesPerSecond);
+            // verify that clsact qdisc has already been created, otherwise attaching a tc police
+            // filter will fail.
+            try {
+                verify(mMockNetd).networkAddInterface(anyInt(), eq(iface));
+            } catch (RemoteException e) {
+                fail(e.getMessage());
+            }
         }
 
         @Override
@@ -2894,6 +2941,7 @@
     @Test
     public void testRequiresValidation() {
         assertTrue(NetworkMonitorUtils.isValidationRequired(
+                NetworkAgentConfigShimImpl.newInstance(null),
                 mCm.getDefaultRequest().networkCapabilities));
     }
 
@@ -3866,14 +3914,14 @@
     }
 
     @Test
-    public void testNoAccessUidsInNetworkRequests() throws Exception {
+    public void testNoAllowedUidsInNetworkRequests() throws Exception {
         final PendingIntent pendingIntent = PendingIntent.getBroadcast(
                 mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
         final NetworkRequest r = new NetworkRequest.Builder().build();
-        final ArraySet<Integer> accessUids = new ArraySet<>();
-        accessUids.add(6);
-        accessUids.add(9);
-        r.networkCapabilities.setAccessUids(accessUids);
+        final ArraySet<Integer> allowedUids = new ArraySet<>();
+        allowedUids.add(6);
+        allowedUids.add(9);
+        r.networkCapabilities.setAllowedUids(allowedUids);
 
         final Handler handler = new Handler(ConnectivityThread.getInstanceLooper());
         final NetworkCallback cb = new NetworkCallback();
@@ -3888,7 +3936,7 @@
 
         // Make sure that resetting the access UIDs to the empty set will allow calling
         // requestNetwork and registerNetworkCallback.
-        r.networkCapabilities.setAccessUids(Collections.emptySet());
+        r.networkCapabilities.setAllowedUids(Collections.emptySet());
         mCm.requestNetwork(r, cb);
         mCm.unregisterNetworkCallback(cb);
         mCm.registerNetworkCallback(r, cb);
@@ -7040,6 +7088,36 @@
     }
 
     @Test
+    public void testAdminUidsRedacted() throws Exception {
+        final int[] adminUids = new int[] {Process.myUid() + 1};
+        final NetworkCapabilities ncTemplate = new NetworkCapabilities();
+        ncTemplate.setAdministratorUids(adminUids);
+        mCellNetworkAgent =
+                new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), ncTemplate);
+        mCellNetworkAgent.connect(false /* validated */);
+
+        // Verify case where caller has permission
+        mServiceContext.setPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED);
+        TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(callback);
+        callback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent);
+        callback.expectCapabilitiesThat(
+                mCellNetworkAgent, nc -> Arrays.equals(adminUids, nc.getAdministratorUids()));
+        mCm.unregisterNetworkCallback(callback);
+
+        // Verify case where caller does NOT have permission
+        mServiceContext.setPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_DENIED);
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_DENIED);
+        callback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(callback);
+        callback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent);
+        callback.expectCapabilitiesThat(
+                mCellNetworkAgent, nc -> nc.getAdministratorUids().length == 0);
+    }
+
+    @Test
     public void testNonVpnUnderlyingNetworks() throws Exception {
         // Ensure wifi and cellular are not torn down.
         for (int transport : new int[]{TRANSPORT_CELLULAR, TRANSPORT_WIFI}) {
@@ -7861,6 +7939,7 @@
         // VPN networks do not satisfy the default request and are automatically validated
         // by NetworkMonitor
         assertFalse(NetworkMonitorUtils.isValidationRequired(
+                NetworkAgentConfigShimImpl.newInstance(mMockVpn.getNetworkAgentConfig()),
                 mMockVpn.getAgent().getNetworkCapabilities()));
         mMockVpn.getAgent().setNetworkValid(false /* isStrictMode */);
 
@@ -8011,6 +8090,7 @@
         assertTrue(nc.hasCapability(NET_CAPABILITY_INTERNET));
 
         assertFalse(NetworkMonitorUtils.isValidationRequired(
+                NetworkAgentConfigShimImpl.newInstance(mMockVpn.getNetworkAgentConfig()),
                 mMockVpn.getAgent().getNetworkCapabilities()));
         assertTrue(NetworkMonitorUtils.isPrivateDnsValidationRequired(
                 mMockVpn.getAgent().getNetworkCapabilities()));
@@ -13830,12 +13910,13 @@
             ProfileNetworkPreference profileNetworkPreference) {
         final Set<UidRange> uidRangeSet;
         UidRange range = UidRange.createForUser(handle);
-        if (profileNetworkPreference.getIncludedUids().size() != 0) {
-            uidRangeSet = UidRangeUtils.convertListToUidRange(
+        if (profileNetworkPreference.getIncludedUids().length != 0) {
+            uidRangeSet = UidRangeUtils.convertArrayToUidRange(
                     profileNetworkPreference.getIncludedUids());
-        } else if (profileNetworkPreference.getExcludedUids().size() != 0)  {
+
+        } else if (profileNetworkPreference.getExcludedUids().length != 0)  {
             uidRangeSet = UidRangeUtils.removeRangeSetFromUidRange(
-                    range, UidRangeUtils.convertListToUidRange(
+                    range, UidRangeUtils.convertArrayToUidRange(
                             profileNetworkPreference.getExcludedUids()));
         } else {
             uidRangeSet = new ArraySet<>();
@@ -14207,7 +14288,7 @@
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder.setIncludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID)});
         registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), false, testHandle,
@@ -14226,7 +14307,7 @@
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder.setIncludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), false,
@@ -14245,7 +14326,7 @@
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder.setExcludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), false,
@@ -14265,7 +14346,7 @@
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder.setExcludedUids(
-                List.of(testHandle.getUid(0) - 1));
+                new int[]{testHandle.getUid(0) - 1});
         final TestOnCompleteListener listener = new TestOnCompleteListener();
         Assert.assertThrows(IllegalArgumentException.class, () -> mCm.setProfileNetworkPreferences(
                 testHandle, List.of(profileNetworkPreferenceBuilder.build()),
@@ -14273,7 +14354,7 @@
 
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder.setIncludedUids(
-                List.of(testHandle.getUid(0) - 1));
+                new int[]{testHandle.getUid(0) - 1});
         Assert.assertThrows(IllegalArgumentException.class,
                 () -> mCm.setProfileNetworkPreferences(
                         testHandle, List.of(profileNetworkPreferenceBuilder.build()),
@@ -14282,9 +14363,9 @@
 
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder.setIncludedUids(
-                List.of(testHandle.getUid(0) - 1));
+                new int[]{testHandle.getUid(0) - 1});
         profileNetworkPreferenceBuilder.setExcludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         Assert.assertThrows(IllegalArgumentException.class,
                 () -> mCm.setProfileNetworkPreferences(
                         testHandle, List.of(profileNetworkPreferenceBuilder.build()),
@@ -14295,9 +14376,9 @@
         profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder2.setIncludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         profileNetworkPreferenceBuilder.setIncludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         Assert.assertThrows(IllegalArgumentException.class,
                 () -> mCm.setProfileNetworkPreferences(
                         testHandle, List.of(profileNetworkPreferenceBuilder.build(),
@@ -14306,9 +14387,9 @@
 
         profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder2.setExcludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         profileNetworkPreferenceBuilder.setExcludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         Assert.assertThrows(IllegalArgumentException.class,
                 () -> mCm.setProfileNetworkPreferences(
                         testHandle, List.of(profileNetworkPreferenceBuilder.build(),
@@ -14318,9 +14399,9 @@
         profileNetworkPreferenceBuilder2.setPreference(
                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
         profileNetworkPreferenceBuilder2.setExcludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         profileNetworkPreferenceBuilder.setExcludedUids(
-                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+                new int[]{testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)});
         Assert.assertThrows(IllegalArgumentException.class,
                 () -> mCm.setProfileNetworkPreferences(
                         testHandle, List.of(profileNetworkPreferenceBuilder.build(),
@@ -14647,7 +14728,7 @@
     }
 
     @Test
-    public void testAccessUids() throws Exception {
+    public void testAllowedUids() throws Exception {
         final int preferenceOrder =
                 ConnectivityService.PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT;
         mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
@@ -14664,7 +14745,7 @@
         final NetworkCapabilities nc = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_TEST)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
-                .setAccessUids(uids)
+                .setAllowedUids(uids)
                 .build();
         final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(TRANSPORT_TEST,
                 new LinkProperties(), nc);
@@ -14682,10 +14763,10 @@
 
         uids.add(300);
         uids.add(400);
-        nc.setAccessUids(uids);
+        nc.setAllowedUids(uids);
         agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(agent, caps -> caps.getAccessUids().equals(uids));
+            cb.expectCapabilitiesThat(agent, caps -> caps.getAllowedUids().equals(uids));
         } else {
             cb.assertNoCallback();
         }
@@ -14699,10 +14780,10 @@
             inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids300400Parcel);
         }
 
-        nc.setAccessUids(uids);
+        nc.setAllowedUids(uids);
         agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(agent, caps -> caps.getAccessUids().equals(uids));
+            cb.expectCapabilitiesThat(agent, caps -> caps.getAllowedUids().equals(uids));
             inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids200Parcel);
         } else {
             cb.assertNoCallback();
@@ -14710,10 +14791,10 @@
 
         uids.clear();
         uids.add(600);
-        nc.setAccessUids(uids);
+        nc.setAllowedUids(uids);
         agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(agent, caps -> caps.getAccessUids().equals(uids));
+            cb.expectCapabilitiesThat(agent, caps -> caps.getAllowedUids().equals(uids));
         } else {
             cb.assertNoCallback();
         }
@@ -14727,10 +14808,10 @@
         }
 
         uids.clear();
-        nc.setAccessUids(uids);
+        nc.setAllowedUids(uids);
         agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(agent, caps -> caps.getAccessUids().isEmpty());
+            cb.expectCapabilitiesThat(agent, caps -> caps.getAllowedUids().isEmpty());
             inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids600Parcel);
         } else {
             cb.assertNoCallback();
@@ -14741,7 +14822,7 @@
     }
 
     @Test
-    public void testCbsAccessUids() throws Exception {
+    public void testCbsAllowedUids() throws Exception {
         mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
         mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
 
@@ -14778,29 +14859,29 @@
                 new LinkProperties(), ncb.build());
         mCellNetworkAgent.connect(true);
         cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
-        ncb.setAccessUids(serviceUidSet);
+        ncb.setAllowedUids(serviceUidSet);
         mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
             cb.expectCapabilitiesThat(mCellNetworkAgent,
-                    caps -> caps.getAccessUids().equals(serviceUidSet));
+                    caps -> caps.getAllowedUids().equals(serviceUidSet));
         } else {
             // S must ignore access UIDs.
             cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         }
 
         // ...but not to some other UID. Rejection sets UIDs to the empty set
-        ncb.setAccessUids(nonServiceUidSet);
+        ncb.setAllowedUids(nonServiceUidSet);
         mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
             cb.expectCapabilitiesThat(mCellNetworkAgent,
-                    caps -> caps.getAccessUids().isEmpty());
+                    caps -> caps.getAllowedUids().isEmpty());
         } else {
             // S must ignore access UIDs.
             cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         }
 
         // ...and also not to multiple UIDs even including the service UID
-        ncb.setAccessUids(serviceUidSetPlus);
+        ncb.setAllowedUids(serviceUidSetPlus);
         mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
 
@@ -14823,7 +14904,7 @@
                 new LinkProperties(), ncb.build());
         mWiFiNetworkAgent.connect(true);
         cb.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
-        ncb.setAccessUids(serviceUidSet);
+        ncb.setAllowedUids(serviceUidSet);
         mWiFiNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         mCm.unregisterNetworkCallback(cb);
@@ -15366,7 +15447,7 @@
                 null /* callingAttributionTag */));
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testUpdateRateLimit_EnableDisable() throws Exception {
         final LinkProperties wifiLp = new LinkProperties();
         wifiLp.setInterfaceName(WIFI_IFNAME);
@@ -15405,7 +15486,7 @@
                 it -> it.first == cellLp.getInterfaceName() && it.second == -1));
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testUpdateRateLimit_WhenNewNetworkIsAdded() throws Exception {
         final LinkProperties wifiLp = new LinkProperties();
         wifiLp.setInterfaceName(WIFI_IFNAME);
@@ -15431,7 +15512,7 @@
                 && it.second == rateLimitInBytesPerSec));
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testUpdateRateLimit_OnlyAffectsInternetCapableNetworks() throws Exception {
         final LinkProperties wifiLp = new LinkProperties();
         wifiLp.setInterfaceName(WIFI_IFNAME);
@@ -15449,7 +15530,7 @@
         assertNull(readHeadWifi.poll(TIMEOUT_MS, it -> it.first == wifiLp.getInterfaceName()));
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testUpdateRateLimit_DisconnectingResetsRateLimit()
             throws Exception {
         // Steps:
@@ -15485,7 +15566,7 @@
         assertNull(readHeadWifi.poll(TIMEOUT_MS, it -> it.first == wifiLp.getInterfaceName()));
     }
 
-    @Test
+    @Test @IgnoreUpTo(SC_V2)
     public void testUpdateRateLimit_UpdateExistingRateLimit() throws Exception {
         final LinkProperties wifiLp = new LinkProperties();
         wifiLp.setInterfaceName(WIFI_IFNAME);
@@ -15514,4 +15595,108 @@
                 it -> it.first == wifiLp.getInterfaceName()
                         && it.second == 2000));
     }
+
+    @Test @IgnoreAfter(SC_V2)
+    public void testUpdateRateLimit_DoesNothingBeforeT() throws Exception {
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+
+        final ArrayTrackRecord<Pair<String, Long>>.ReadHead readHead =
+                mDeps.mRateLimitHistory.newReadHead();
+
+        setIngressRateLimit(1000);
+        waitForIdle();
+
+        assertNull(readHead.poll(TEST_CALLBACK_TIMEOUT_MS, it -> true));
+    }
+
+    @Test
+    public void testIgnoreValidationAfterRoamDisabled() throws Exception {
+        assumeFalse(SdkLevel.isAtLeastT());
+        // testIgnoreValidationAfterRoam off
+        doReturn(-1).when(mResources)
+                .getInteger(R.integer.config_validationFailureAfterRoamIgnoreTimeMillis);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        NetworkCapabilities wifiNc1 = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .setTransportInfo(new WifiInfo.Builder().setBssid("AA:AA:AA:AA:AA:AA").build());
+        NetworkCapabilities wifiNc2 = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .setTransportInfo(new WifiInfo.Builder().setBssid("BB:BB:BB:BB:BB:BB").build());
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc1);
+        mWiFiNetworkAgent.connect(true);
+
+        // The default network will be switching to Wi-Fi Network.
+        final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+        wifiNetworkCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        registerDefaultNetworkCallbacks();
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+
+        // Wi-Fi roaming from wifiNc1 to wifiNc2.
+        mWiFiNetworkAgent.setNetworkCapabilities(wifiNc2, true);
+        mWiFiNetworkAgent.setNetworkInvalid(false);
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+    }
+
+    @Test
+    public void testIgnoreValidationAfterRoamEnabled() throws Exception {
+        assumeFalse(SdkLevel.isAtLeastT());
+        // testIgnoreValidationAfterRoam on
+        doReturn(5000).when(mResources)
+                .getInteger(R.integer.config_validationFailureAfterRoamIgnoreTimeMillis);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        NetworkCapabilities wifiNc1 = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .setTransportInfo(new WifiInfo.Builder().setBssid("AA:AA:AA:AA:AA:AA").build());
+        NetworkCapabilities wifiNc2 = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .setTransportInfo(new WifiInfo.Builder().setBssid("BB:BB:BB:BB:BB:BB").build());
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc1);
+        mWiFiNetworkAgent.connect(true);
+
+        // The default network will be switching to Wi-Fi Network.
+        final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+        wifiNetworkCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        registerDefaultNetworkCallbacks();
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+
+        // Wi-Fi roaming from wifiNc1 to wifiNc2.
+        mWiFiNetworkAgent.setNetworkCapabilities(wifiNc2, true);
+        mWiFiNetworkAgent.setNetworkInvalid(false);
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
+
+        // Network validation failed, but the result will be ignored.
+        assertTrue(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mWiFiNetworkAgent.setNetworkValid(false);
+
+        // Behavior of after config_validationFailureAfterRoamIgnoreTimeMillis
+        ConditionVariable waitForValidationBlock = new ConditionVariable();
+        doReturn(50).when(mResources)
+                .getInteger(R.integer.config_validationFailureAfterRoamIgnoreTimeMillis);
+        // Wi-Fi roaming from wifiNc2 to wifiNc1.
+        mWiFiNetworkAgent.setNetworkCapabilities(wifiNc1, true);
+        mWiFiNetworkAgent.setNetworkInvalid(false);
+        waitForValidationBlock.block(150);
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+    }
 }
diff --git a/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt
index 64736f2..7ed55e5 100644
--- a/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt
+++ b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt
@@ -23,6 +23,8 @@
 
 import android.content.Context
 import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_ETHERNET
+import android.content.pm.PackageManager.FEATURE_USB_HOST
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
 import android.net.ConnectivityManager.TYPE_ETHERNET
@@ -40,7 +42,6 @@
 import android.net.ConnectivityManager.TYPE_WIFI
 import android.net.ConnectivityManager.TYPE_WIFI_P2P
 import android.net.ConnectivityManager.TYPE_WIMAX
-import android.net.EthernetManager
 import android.net.NetworkInfo.DetailedState.CONNECTED
 import android.net.NetworkInfo.DetailedState.DISCONNECTED
 import android.os.Build
@@ -82,9 +83,8 @@
     private val mContext = mock(Context::class.java).apply {
         doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_WIFI)
         doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_WIFI_DIRECT)
+        doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_ETHERNET)
         doReturn(mPm).`when`(this).packageManager
-        doReturn(mock(EthernetManager::class.java)).`when`(this).getSystemService(
-                Context.ETHERNET_SERVICE)
     }
     private val mTm = mock(TelephonyManager::class.java).apply {
         doReturn(true).`when`(this).isDataCapable
@@ -105,7 +105,8 @@
 
     @Test
     fun testSupportedTypes_NoEthernet() {
-        doReturn(null).`when`(mContext).getSystemService(Context.ETHERNET_SERVICE)
+        doReturn(false).`when`(mPm).hasSystemFeature(FEATURE_ETHERNET)
+        doReturn(false).`when`(mPm).hasSystemFeature(FEATURE_USB_HOST)
         assertFalse(makeTracker().isTypeSupported(TYPE_ETHERNET))
     }
 
diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
index a3b0e7c..7688a6b 100644
--- a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
+++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
@@ -250,7 +250,7 @@
         mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true);
         assertTrue("Should be true since mobile data usage is restricted",
                 mNMService.isNetworkRestricted(TEST_UID));
-        verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* enabled */);
+        verify(mCm).addUidToMeteredNetworkDenyList(TEST_UID);
 
         mNMService.setDataSaverModeEnabled(true);
         verify(mNetdService).bandwidthEnableDataSaver(true);
@@ -258,16 +258,16 @@
         mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false);
         assertTrue("Should be true since data saver is on and the uid is not allowlisted",
                 mNMService.isNetworkRestricted(TEST_UID));
-        verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* false */);
+        verify(mCm).removeUidFromMeteredNetworkDenyList(TEST_UID);
 
         mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true);
         assertFalse("Should be false since data saver is on and the uid is allowlisted",
                 mNMService.isNetworkRestricted(TEST_UID));
-        verify(mCm).updateMeteredNetworkAllowList(TEST_UID, true /* enabled */);
+        verify(mCm).addUidToMeteredNetworkAllowList(TEST_UID);
 
         // remove uid from allowlist and turn datasaver off again
         mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
-        verify(mCm).updateMeteredNetworkAllowList(TEST_UID, false /* enabled */);
+        verify(mCm).removeUidFromMeteredNetworkAllowList(TEST_UID);
         mNMService.setDataSaverModeEnabled(false);
         verify(mNetdService).bandwidthEnableDataSaver(false);
         assertFalse("Network should not be restricted when data saver is off",
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 5086943..3c228d0 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -19,10 +19,12 @@
 import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -36,6 +38,7 @@
 import android.content.Context;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
+import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Binder;
@@ -49,9 +52,6 @@
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
-import com.android.server.NsdService.DaemonConnection;
-import com.android.server.NsdService.DaemonConnectionSupplier;
-import com.android.server.NsdService.NativeCallbackReceiver;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
@@ -63,10 +63,8 @@
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
 
 import java.util.LinkedList;
 import java.util.Queue;
@@ -92,8 +90,7 @@
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
     @Mock Context mContext;
     @Mock ContentResolver mResolver;
-    NativeCallbackReceiver mDaemonCallback;
-    @Spy DaemonConnection mDaemon = new DaemonConnection(mDaemonCallback);
+    @Mock MDnsManager mMockMDnsM;
     HandlerThread mThread;
     TestHandler mHandler;
 
@@ -112,9 +109,17 @@
         MockitoAnnotations.initMocks(this);
         mThread = new HandlerThread("mock-service-handler");
         mThread.start();
-        doReturn(true).when(mDaemon).execute(any());
         mHandler = new TestHandler(mThread.getLooper());
         when(mContext.getContentResolver()).thenReturn(mResolver);
+        doReturn(MDnsManager.MDNS_SERVICE).when(mContext)
+                .getSystemServiceName(MDnsManager.class);
+        doReturn(mMockMDnsM).when(mContext).getSystemService(MDnsManager.MDNS_SERVICE);
+        doReturn(true).when(mMockMDnsM).registerService(
+                anyInt(), anyString(), anyString(), anyInt(), any(), anyInt());
+        doReturn(true).when(mMockMDnsM).stopOperation(anyInt());
+        doReturn(true).when(mMockMDnsM).discover(anyInt(), anyString(), anyInt());
+        doReturn(true).when(mMockMDnsM).resolve(
+                anyInt(), anyString(), anyString(), anyString(), anyInt());
     }
 
     @After
@@ -135,24 +140,25 @@
         waitForIdle();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
-        verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommands("start-service");
+        verify(mMockMDnsM, times(1)).registerEventListener(any());
+        verify(mMockMDnsM, times(1)).startDaemon();
 
         connectClient(service);
         waitForIdle();
         final INsdManagerCallback cb2 = getCallback();
         final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
-        verify(mDaemon, times(1)).maybeStart();
+        // Daemon has been started, it should not try to start it again.
+        verify(mMockMDnsM, times(1)).registerEventListener(any());
+        verify(mMockMDnsM, times(1)).startDaemon();
 
         deathRecipient1.binderDied();
         // Still 1 client remains, daemon shouldn't be stopped.
         waitForIdle();
-        verify(mDaemon, never()).maybeStop();
+        verify(mMockMDnsM, never()).stopDaemon();
 
         deathRecipient2.binderDied();
         // All clients are disconnected, the daemon should be stopped.
         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
-        verifyDaemonCommands("stop-service");
     }
 
     @Test
@@ -160,28 +166,30 @@
     public void testNoDaemonStartedWhenClientsConnect() throws Exception {
         final NsdService service = makeService();
 
-        // Creating an NsdManager will not cause any cmds executed, which means
-        // no daemon is started.
+        // Creating an NsdManager will not cause daemon startup.
         connectClient(service);
         waitForIdle();
-        verify(mDaemon, never()).execute(any());
+        verify(mMockMDnsM, never()).registerEventListener(any());
+        verify(mMockMDnsM, never()).startDaemon();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
 
-        // Creating another NsdManager will not cause any cmds executed.
+        // Creating another NsdManager will not cause daemon startup either.
         connectClient(service);
         waitForIdle();
-        verify(mDaemon, never()).execute(any());
+        verify(mMockMDnsM, never()).registerEventListener(any());
+        verify(mMockMDnsM, never()).startDaemon();
         final INsdManagerCallback cb2 = getCallback();
         final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
 
-        // If there is no active request, try to clean up the daemon
-        // every time the client disconnects.
+        // If there is no active request, try to clean up the daemon but should not do it because
+        // daemon has not been started.
         deathRecipient1.binderDied();
-        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
-        reset(mDaemon);
+        verify(mMockMDnsM, never()).unregisterEventListener(any());
+        verify(mMockMDnsM, never()).stopDaemon();
         deathRecipient2.binderDied();
-        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+        verify(mMockMDnsM, never()).unregisterEventListener(any());
+        verify(mMockMDnsM, never()).stopDaemon();
     }
 
     private IBinder.DeathRecipient verifyLinkToDeath(INsdManagerCallback cb)
@@ -200,8 +208,8 @@
         waitForIdle();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
-        verify(mDaemon, never()).maybeStart();
-        verify(mDaemon, never()).execute(any());
+        verify(mMockMDnsM, never()).registerEventListener(any());
+        verify(mMockMDnsM, never()).startDaemon();
 
         NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
         request.setPort(2201);
@@ -210,29 +218,31 @@
         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
         client.registerService(request, PROTOCOL, listener1);
         waitForIdle();
-        verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommands("start-service", "register 2 a_name a_type 2201");
+        verify(mMockMDnsM, times(1)).registerEventListener(any());
+        verify(mMockMDnsM, times(1)).startDaemon();
+        verify(mMockMDnsM, times(1)).registerService(
+                eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
 
         // Client discovery request
         NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
         client.discoverServices("a_type", PROTOCOL, listener2);
         waitForIdle();
-        verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommand("discover 3 a_type 0");
+        verify(mMockMDnsM, times(1)).discover(eq(3), eq("a_type"), eq(0));
 
         // Client resolve request
         NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
         client.resolveService(request, listener3);
         waitForIdle();
-        verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommand("resolve 4 a_name a_type local. 0");
+        verify(mMockMDnsM, times(1)).resolve(
+                eq(4), eq("a_name"), eq("a_type"), eq("local."), eq(0));
 
         // Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
         deathRecipient.binderDied();
         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
         // checks that request are cleaned
-        verifyDaemonCommands("stop-register 2", "stop-discover 3",
-                "stop-resolve 4", "stop-service");
+        verify(mMockMDnsM, times(1)).stopOperation(eq(2));
+        verify(mMockMDnsM, times(1)).stopOperation(eq(3));
+        verify(mMockMDnsM, times(1)).stopOperation(eq(4));
     }
 
     @Test
@@ -246,20 +256,23 @@
         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
         client.registerService(request, PROTOCOL, listener1);
         waitForIdle();
-        verify(mDaemon, times(1)).maybeStart();
+        verify(mMockMDnsM, times(1)).registerEventListener(any());
+        verify(mMockMDnsM, times(1)).startDaemon();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
-        verifyDaemonCommands("start-service", "register 2 a_name a_type 2201");
+        verify(mMockMDnsM, times(1)).registerService(
+                eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
 
         client.unregisterService(listener1);
-        verifyDaemonCommand("stop-register 2");
+        waitForIdle();
+        verify(mMockMDnsM, times(1)).stopOperation(eq(2));
 
         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
-        verifyDaemonCommand("stop-service");
-        reset(mDaemon);
+        reset(mMockMDnsM);
         deathRecipient.binderDied();
-        // Client disconnects, after CLEANUP_DELAY_MS, maybeStop the daemon.
-        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+        // Client disconnects, daemon should not be stopped after CLEANUP_DELAY_MS.
+        verify(mMockMDnsM, never()).unregisterEventListener(any());
+        verify(mMockMDnsM, never()).stopDaemon();
     }
 
     private void waitForIdle() {
@@ -267,11 +280,7 @@
     }
 
     NsdService makeService() {
-        DaemonConnectionSupplier supplier = (callback) -> {
-            mDaemonCallback = callback;
-            return mDaemon;
-        };
-        final NsdService service = new NsdService(mContext, mHandler, supplier, CLEANUP_DELAY_MS) {
+        final NsdService service = new NsdService(mContext, mHandler, CLEANUP_DELAY_MS) {
             @Override
             public INsdServiceConnector connect(INsdManagerCallback baseCb) {
                 // Wrap the callback in a transparent mock, to mock asBinder returning a
@@ -285,7 +294,6 @@
                 return super.connect(cb);
             }
         };
-        verify(mDaemon, never()).execute(any(String.class));
         return service;
     }
 
@@ -297,34 +305,15 @@
         return new NsdManager(mContext, service);
     }
 
-    void verifyDelayMaybeStopDaemon(long cleanupDelayMs) {
+    void verifyDelayMaybeStopDaemon(long cleanupDelayMs) throws Exception {
         waitForIdle();
         // Stop daemon shouldn't be called immediately.
-        verify(mDaemon, never()).maybeStop();
+        verify(mMockMDnsM, never()).unregisterEventListener(any());
+        verify(mMockMDnsM, never()).stopDaemon();
+
         // Clean up the daemon after CLEANUP_DELAY_MS.
-        verify(mDaemon, timeout(cleanupDelayMs + TIMEOUT_MS)).maybeStop();
-    }
-
-    void verifyDaemonCommands(String... wants) {
-        verifyDaemonCommand(String.join(" ", wants), wants.length);
-    }
-
-    void verifyDaemonCommand(String want) {
-        verifyDaemonCommand(want, 1);
-    }
-
-    void verifyDaemonCommand(String want, int n) {
-        waitForIdle();
-        final ArgumentCaptor<Object> argumentsCaptor = ArgumentCaptor.forClass(Object.class);
-        verify(mDaemon, times(n)).execute(argumentsCaptor.capture());
-        String got = "";
-        for (Object o : argumentsCaptor.getAllValues()) {
-            got += o + " ";
-        }
-        assertEquals(want, got.trim());
-        // rearm deamon for next command verification
-        reset(mDaemon);
-        doReturn(true).when(mDaemon).execute(any());
+        verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).unregisterEventListener(any());
+        verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).stopDaemon();
     }
 
     public static class TestHandler extends Handler {
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 553cb83..157507b 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -69,8 +69,6 @@
 @RunWith(DevSdkIgnoreRunner.class)
 @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
 public class CarrierPrivilegeAuthenticatorTest {
-    // TODO : use ConstantsShim.RECEIVER_NOT_EXPORTED when it's available in tests.
-    private static final int RECEIVER_NOT_EXPORTED = 4;
     private static final int SUBSCRIPTION_COUNT = 2;
     private static final int TEST_SUBSCRIPTION_ID = 1;
 
@@ -117,7 +115,7 @@
 
     private IntentFilter getIntentFilter() {
         final ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class);
-        verify(mContext).registerReceiver(any(), captor.capture(), any(), any(), anyInt());
+        verify(mContext).registerReceiver(any(), captor.capture(), any(), any());
         return captor.getValue();
     }
 
@@ -140,11 +138,10 @@
     @Test
     public void testConstructor() throws Exception {
         verify(mContext).registerReceiver(
-                eq(mCarrierPrivilegeAuthenticator),
-                any(IntentFilter.class),
-                any(),
-                any(),
-                eq(RECEIVER_NOT_EXPORTED));
+                        eq(mCarrierPrivilegeAuthenticator),
+                        any(IntentFilter.class),
+                        any(),
+                        any());
         final IntentFilter filter = getIntentFilter();
         assertEquals(1, filter.countActions());
         assertTrue(filter.hasAction(ACTION_MULTI_SIM_CONFIG_CHANGED));
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index 84e02ce..c3d64cb 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -17,31 +17,40 @@
 package com.android.server.connectivity;
 
 import static android.net.INetd.IF_STATE_UP;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
 import static com.android.server.connectivity.ClatCoordinator.CLAT_MAX_MTU;
+import static com.android.server.connectivity.ClatCoordinator.EGRESS;
+import static com.android.server.connectivity.ClatCoordinator.INGRESS;
 import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_PREFIX_LEN;
 import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_STRING;
+import static com.android.server.connectivity.ClatCoordinator.PRIO_CLAT;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.never;
 
 import android.annotation.NonNull;
 import android.net.INetd;
+import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.bpf.ClatEgress4Key;
+import com.android.net.module.util.bpf.ClatEgress4Value;
+import com.android.net.module.util.bpf.ClatIngress6Key;
+import com.android.net.module.util.bpf.ClatIngress6Value;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -55,6 +64,8 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.util.Objects;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -64,9 +75,12 @@
     private static final String BASE_IFACE = "test0";
     private static final String STACKED_IFACE = "v4-test0";
     private static final int BASE_IFINDEX = 1000;
+    private static final int STACKED_IFINDEX = 1001;
 
     private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
     private static final String NAT64_PREFIX_STRING = "64:ff9b::";
+    private static final Inet6Address INET6_PFX96 = (Inet6Address)
+            InetAddresses.parseNumericAddress(NAT64_PREFIX_STRING);
     private static final int GOOGLE_DNS_4 = 0x08080808;  // 8.8.8.8
     private static final int NETID = 42;
 
@@ -77,11 +91,16 @@
 
     private static final String XLAT_LOCAL_IPV4ADDR_STRING = "192.0.0.46";
     private static final String XLAT_LOCAL_IPV6ADDR_STRING = "2001:db8:0:b11::464";
+    private static final Inet4Address INET4_LOCAL4 = (Inet4Address)
+            InetAddresses.parseNumericAddress(XLAT_LOCAL_IPV4ADDR_STRING);
+    private static final Inet6Address INET6_LOCAL6 = (Inet6Address)
+            InetAddresses.parseNumericAddress(XLAT_LOCAL_IPV6ADDR_STRING);
     private static final int CLATD_PID = 10483;
 
     private static final int TUN_FD = 534;
     private static final int RAW_SOCK_FD = 535;
     private static final int PACKET_SOCK_FD = 536;
+    private static final long RAW_SOCK_COOKIE = 27149;
     private static final ParcelFileDescriptor TUN_PFD = new ParcelFileDescriptor(
             new FileDescriptor());
     private static final ParcelFileDescriptor RAW_SOCK_PFD = new ParcelFileDescriptor(
@@ -89,8 +108,23 @@
     private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
             new FileDescriptor());
 
+    private static final String EGRESS_PROG_PATH =
+            "/sys/fs/bpf/prog_clatd_schedcls_egress4_clat_rawip";
+    private static final String INGRESS_PROG_PATH =
+            "/sys/fs/bpf/prog_clatd_schedcls_ingress6_clat_ether";
+    private static final ClatEgress4Key EGRESS_KEY = new ClatEgress4Key(STACKED_IFINDEX,
+            INET4_LOCAL4);
+    private static final ClatEgress4Value EGRESS_VALUE = new ClatEgress4Value(BASE_IFINDEX,
+            INET6_LOCAL6, INET6_PFX96, (short) 1 /* oifIsEthernet, 1 = true */);
+    private static final ClatIngress6Key INGRESS_KEY = new ClatIngress6Key(BASE_IFINDEX,
+            INET6_PFX96, INET6_LOCAL6);
+    private static final ClatIngress6Value INGRESS_VALUE = new ClatIngress6Value(STACKED_IFINDEX,
+            INET4_LOCAL4);
+
     @Mock private INetd mNetd;
     @Spy private TestDependencies mDeps = new TestDependencies();
+    @Mock private IBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap;
+    @Mock private IBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap;
 
     /**
       * The dependency injection class is used to mock the JNI functions and system functions
@@ -131,6 +165,8 @@
         public int getInterfaceIndex(String ifName) {
             if (BASE_IFACE.equals(ifName)) {
                 return BASE_IFINDEX;
+            } else if (STACKED_IFACE.equals(ifName)) {
+                return STACKED_IFINDEX;
             }
             fail("unsupported arg: " + ifName);
             return -1;
@@ -258,12 +294,78 @@
         /**
          * Stop clatd.
          */
+        @Override
         public void stopClatd(@NonNull String iface, @NonNull String pfx96, @NonNull String v4,
                 @NonNull String v6, int pid) throws IOException {
             if (pid == -1) {
                 fail("unsupported arg: " + pid);
             }
         }
+
+        /**
+         * Tag socket as clat.
+         */
+        @Override
+        public long tagSocketAsClat(@NonNull FileDescriptor sock) throws IOException {
+            if (Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), sock)) {
+                return RAW_SOCK_COOKIE;
+            }
+            fail("unsupported arg: " + sock);
+            return 0;
+        }
+
+        /**
+         * Untag socket.
+         */
+        @Override
+        public void untagSocket(long cookie) throws IOException {
+            if (cookie != RAW_SOCK_COOKIE) {
+                fail("unsupported arg: " + cookie);
+            }
+        }
+
+        /** Get ingress6 BPF map. */
+        @Override
+        public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
+            return mIngressMap;
+        }
+
+        /** Get egress4 BPF map. */
+        @Override
+        public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
+            return mEgressMap;
+        }
+
+        /** Checks if the network interface uses an ethernet L2 header. */
+        public boolean isEthernet(String iface) throws IOException {
+            if (BASE_IFACE.equals(iface)) return true;
+
+            fail("unsupported arg: " + iface);
+            return false;
+        }
+
+        /** Add a clsact qdisc. */
+        @Override
+        public void tcQdiscAddDevClsact(int ifIndex) throws IOException {
+            // no-op
+            return;
+        }
+
+        /** Attach a tc bpf filter. */
+        @Override
+        public void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, short proto,
+                String bpfProgPath) throws IOException {
+            // no-op
+            return;
+        }
+
+        /** Delete a tc filter. */
+        @Override
+        public void tcFilterDelDev(int ifIndex, boolean ingress, short prio, short proto)
+                throws IOException {
+            // no-op
+            return;
+        }
     };
 
     @NonNull
@@ -288,12 +390,17 @@
     @Test
     public void testStartStopClatd() throws Exception {
         final ClatCoordinator coordinator = makeClatCoordinator();
-        final InOrder inOrder = inOrder(mNetd, mDeps);
-        clearInvocations(mNetd, mDeps);
+        final InOrder inOrder = inOrder(mNetd, mDeps, mIngressMap, mEgressMap);
+        clearInvocations(mNetd, mDeps, mIngressMap, mEgressMap);
 
         // [1] Start clatd.
         final String addr6For464xlat = coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
         assertEquals(XLAT_LOCAL_IPV6ADDR_STRING, addr6For464xlat);
+        final ClatCoordinator.ClatdTracker expected = new ClatCoordinator.ClatdTracker(
+                BASE_IFACE, BASE_IFINDEX, STACKED_IFACE, STACKED_IFINDEX,
+                INET4_LOCAL4, INET6_LOCAL6, INET6_PFX96, CLATD_PID, RAW_SOCK_COOKIE);
+        final ClatCoordinator.ClatdTracker actual = coordinator.getClatdTrackerForTesting();
+        assertEquals(expected, actual);
 
         // Pick an IPv4 address.
         inOrder.verify(mDeps).selectIpv4Address(eq(INIT_V4ADDR_STRING),
@@ -306,6 +413,7 @@
         // Open, configure and bring up the tun interface.
         inOrder.verify(mDeps).createTunInterface(eq(STACKED_IFACE));
         inOrder.verify(mDeps).adoptFd(eq(TUN_FD));
+        inOrder.verify(mDeps).getInterfaceIndex(eq(STACKED_IFACE));
         inOrder.verify(mNetd).interfaceSetEnableIPv6(eq(STACKED_IFACE), eq(false /* enable */));
         inOrder.verify(mDeps).detectMtu(eq(NAT64_PREFIX_STRING), eq(GOOGLE_DNS_4), eq(MARK));
         inOrder.verify(mNetd).interfaceSetMtu(eq(STACKED_IFACE),
@@ -326,6 +434,8 @@
         inOrder.verify(mDeps).addAnycastSetsockopt(
                 argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
                 eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+        inOrder.verify(mDeps).tagSocketAsClat(
+                argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)));
         inOrder.verify(mDeps).configurePacketSocket(
                 argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
                 eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
@@ -337,6 +447,13 @@
                 argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
                 eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
                 eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING));
+        inOrder.verify(mEgressMap).insertEntry(eq(EGRESS_KEY), eq(EGRESS_VALUE));
+        inOrder.verify(mIngressMap).insertEntry(eq(INGRESS_KEY), eq(INGRESS_VALUE));
+        inOrder.verify(mDeps).tcQdiscAddDevClsact(eq(STACKED_IFINDEX));
+        inOrder.verify(mDeps).tcFilterAddDevBpf(eq(STACKED_IFINDEX), eq(EGRESS),
+                eq((short) PRIO_CLAT), eq((short) ETH_P_IP), eq(EGRESS_PROG_PATH));
+        inOrder.verify(mDeps).tcFilterAddDevBpf(eq(BASE_IFINDEX), eq(INGRESS),
+                eq((short) PRIO_CLAT), eq((short) ETH_P_IPV6), eq(INGRESS_PROG_PATH));
         inOrder.verifyNoMoreInteractions();
 
         // [2] Start clatd again failed.
@@ -346,15 +463,21 @@
 
         // [3] Expect clatd to stop successfully.
         coordinator.clatStop();
+        inOrder.verify(mDeps).tcFilterDelDev(eq(BASE_IFINDEX), eq(INGRESS),
+                eq((short) PRIO_CLAT), eq((short) ETH_P_IPV6));
+        inOrder.verify(mDeps).tcFilterDelDev(eq(STACKED_IFINDEX), eq(EGRESS),
+                eq((short) PRIO_CLAT), eq((short) ETH_P_IP));
+        inOrder.verify(mEgressMap).deleteEntry(eq(EGRESS_KEY));
+        inOrder.verify(mIngressMap).deleteEntry(eq(INGRESS_KEY));
         inOrder.verify(mDeps).stopClatd(eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
                 eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(CLATD_PID));
+        inOrder.verify(mDeps).untagSocket(eq(RAW_SOCK_COOKIE));
+        assertNull(coordinator.getClatdTrackerForTesting());
         inOrder.verifyNoMoreInteractions();
 
         // [4] Expect an IO exception while stopping a clatd that doesn't exist.
         assertThrows("java.io.IOException: Clatd has not started", IOException.class,
                 () -> coordinator.clatStop());
-        inOrder.verify(mDeps, never()).stopClatd(anyString(), anyString(), anyString(),
-                anyString(), anyInt());
         inOrder.verifyNoMoreInteractions();
     }
 
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index 785153a..c03a9cd 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -22,19 +22,23 @@
 import android.os.Build
 import android.text.TextUtils
 import android.util.ArraySet
+import android.util.Log
 import androidx.test.filters.SmallTest
 import com.android.server.connectivity.FullScore.MAX_CS_MANAGED_POLICY
 import com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED
 import com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED
+import com.android.server.connectivity.FullScore.POLICY_IS_DESTROYED
+import com.android.server.connectivity.FullScore.POLICY_IS_UNMETERED
 import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED
 import com.android.server.connectivity.FullScore.POLICY_IS_VPN
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.After
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.reflect.full.staticProperties
 import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
@@ -47,7 +51,8 @@
         validated: Boolean = false,
         vpn: Boolean = false,
         onceChosen: Boolean = false,
-        acceptUnvalidated: Boolean = false
+        acceptUnvalidated: Boolean = false,
+        destroyed: Boolean = false
     ): FullScore {
         val nac = NetworkAgentConfig.Builder().apply {
             setUnvalidatedConnectivityAcceptable(acceptUnvalidated)
@@ -57,7 +62,24 @@
             if (vpn) addTransportType(NetworkCapabilities.TRANSPORT_VPN)
             if (validated) addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
         }.build()
-        return mixInScore(nc, nac, validated, false /* yieldToBadWifi */)
+        return mixInScore(nc, nac, validated, false /* yieldToBadWifi */, destroyed)
+    }
+
+    private val TAG = this::class.simpleName
+
+    private var wtfHandler: Log.TerribleFailureHandler? = null
+
+    @Before
+    fun setUp() {
+        // policyNameOf will call Log.wtf if passed an invalid policy.
+        wtfHandler = Log.setWtfHandler() { tagString, what, system ->
+            Log.d(TAG, "WTF captured, ignoring: $tagString $what")
+        }
+    }
+
+    @After
+    fun tearDown() {
+        Log.setWtfHandler(wtfHandler)
     }
 
     @Test
@@ -98,9 +120,9 @@
             assertFalse(foundNames.contains(name))
             foundNames.add(name)
         }
-        assertFailsWith<IllegalArgumentException> {
-            FullScore.policyNameOf(MAX_CS_MANAGED_POLICY + 1)
-        }
+        assertEquals("IS_UNMETERED", FullScore.policyNameOf(POLICY_IS_UNMETERED))
+        val invalidPolicy = MAX_CS_MANAGED_POLICY + 1
+        assertEquals(Integer.toString(invalidPolicy), FullScore.policyNameOf(invalidPolicy))
     }
 
     fun getAllPolicies() = Regex("POLICY_.*").let { nameRegex ->
@@ -118,6 +140,7 @@
         assertTrue(ns.withPolicies(vpn = true).hasPolicy(POLICY_IS_VPN))
         assertTrue(ns.withPolicies(onceChosen = true).hasPolicy(POLICY_EVER_USER_SELECTED))
         assertTrue(ns.withPolicies(acceptUnvalidated = true).hasPolicy(POLICY_ACCEPT_UNVALIDATED))
+        assertTrue(ns.withPolicies(destroyed = true).hasPolicy(POLICY_IS_DESTROYED))
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 6590543..fb821c3 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -77,6 +77,7 @@
 import android.net.UidRange;
 import android.net.Uri;
 import android.os.Build;
+import android.os.Process;
 import android.os.SystemConfigManager;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -88,7 +89,10 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
+import com.android.networkstack.apishim.ProcessShimImpl;
+import com.android.networkstack.apishim.common.ProcessShim;
 import com.android.server.BpfNetMaps;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -113,23 +117,32 @@
 public class PermissionMonitorTest {
     private static final int MOCK_USER_ID1 = 0;
     private static final int MOCK_USER_ID2 = 1;
+    private static final int MOCK_USER_ID3 = 2;
     private static final UserHandle MOCK_USER1 = UserHandle.of(MOCK_USER_ID1);
     private static final UserHandle MOCK_USER2 = UserHandle.of(MOCK_USER_ID2);
+    private static final UserHandle MOCK_USER3 = UserHandle.of(MOCK_USER_ID3);
     private static final int MOCK_APPID1 = 10001;
     private static final int MOCK_APPID2 = 10086;
+    private static final int MOCK_APPID3 = 10110;
     private static final int SYSTEM_APPID1 = 1100;
     private static final int SYSTEM_APPID2 = 1108;
     private static final int VPN_APPID = 10002;
     private static final int MOCK_UID11 = MOCK_USER1.getUid(MOCK_APPID1);
     private static final int MOCK_UID12 = MOCK_USER1.getUid(MOCK_APPID2);
+    private static final int MOCK_UID13 = MOCK_USER1.getUid(MOCK_APPID3);
     private static final int SYSTEM_APP_UID11 = MOCK_USER1.getUid(SYSTEM_APPID1);
     private static final int VPN_UID = MOCK_USER1.getUid(VPN_APPID);
     private static final int MOCK_UID21 = MOCK_USER2.getUid(MOCK_APPID1);
     private static final int MOCK_UID22 = MOCK_USER2.getUid(MOCK_APPID2);
+    private static final int MOCK_UID23 = MOCK_USER2.getUid(MOCK_APPID3);
     private static final int SYSTEM_APP_UID21 = MOCK_USER2.getUid(SYSTEM_APPID1);
+    private static final int MOCK_UID31 = MOCK_USER3.getUid(MOCK_APPID1);
+    private static final int MOCK_UID32 = MOCK_USER3.getUid(MOCK_APPID2);
+    private static final int MOCK_UID33 = MOCK_USER3.getUid(MOCK_APPID3);
     private static final String REAL_SYSTEM_PACKAGE_NAME = "android";
     private static final String MOCK_PACKAGE1 = "appName1";
     private static final String MOCK_PACKAGE2 = "appName2";
+    private static final String MOCK_PACKAGE3 = "appName3";
     private static final String SYSTEM_PACKAGE1 = "sysName1";
     private static final String SYSTEM_PACKAGE2 = "sysName2";
     private static final String PARTITION_SYSTEM = "system";
@@ -153,6 +166,8 @@
     private NetdMonitor mNetdMonitor;
     private BpfMapMonitor mBpfMapMonitor;
 
+    private ProcessShim mProcessShim = ProcessShimImpl.newInstance();
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -185,6 +200,7 @@
         mBpfMapMonitor = new BpfMapMonitor(mBpfNetMaps);
 
         doReturn(List.of()).when(mPackageManager).getInstalledPackagesAsUser(anyInt(), anyInt());
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
     }
 
     private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion,
@@ -197,6 +213,10 @@
         return mPermissionMonitor.hasRestrictedNetworkPermission(packageInfo);
     }
 
+    private boolean hasSdkSandbox(final int uid) {
+        return SdkLevel.isAtLeastT() && Process.isApplicationUid(uid);
+    }
+
     private static PackageInfo systemPackageInfoWithPermissions(String... permissions) {
         return packageInfoWithPermissions(
                 REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM);
@@ -273,6 +293,18 @@
         mPermissionMonitor.onPackageAdded(packageName, uid);
     }
 
+    private void removePackage(String packageName, int uid) {
+        final String[] oldPackages = mPackageManager.getPackagesForUid(uid);
+        // If the package isn't existed, no need to remove it.
+        if (!CollectionUtils.contains(oldPackages, packageName)) return;
+
+        // Remove the package if this uid is shared with other packages.
+        final String[] newPackages = Arrays.stream(oldPackages).filter(e -> !e.equals(packageName))
+                .toArray(String[]::new);
+        doReturn(newPackages).when(mPackageManager).getPackagesForUid(eq(uid));
+        mPermissionMonitor.onPackageRemoved(packageName, uid);
+    }
+
     @Test
     public void testHasPermission() {
         PackageInfo app = systemPackageInfoWithPermissions();
@@ -493,6 +525,11 @@
             String... permissions) throws Exception {
         addPackage(name, uid, permissions);
         assertEquals(hasPermission, mPermissionMonitor.hasUseBackgroundNetworksPermission(uid));
+        if (hasSdkSandbox(uid)) {
+            final int sdkSandboxUid = mProcessShim.toSdkSandboxUid(uid);
+            assertEquals(hasPermission,
+                    mPermissionMonitor.hasUseBackgroundNetworksPermission(sdkSandboxUid));
+        }
     }
 
     @Test
@@ -531,7 +568,7 @@
             }).when(mockBpfmap).setNetPermForUids(anyInt(), any(int[].class));
         }
 
-        public void expectTrafficPerm(int permission, int... appIds) {
+        public void expectTrafficPerm(int permission, Integer... appIds) {
             for (final int appId : appIds) {
                 if (mAppIdsTrafficPermission.get(appId, DOES_NOT_EXIST) == DOES_NOT_EXIST) {
                     fail("appId " + appId + " does not exist.");
@@ -540,6 +577,17 @@
                     fail("appId " + appId + " has wrong permission: "
                             + mAppIdsTrafficPermission.get(appId));
                 }
+                if (hasSdkSandbox(appId)) {
+                    int sdkSandboxAppId = mProcessShim.toSdkSandboxUid(appId);
+                    if (mAppIdsTrafficPermission.get(sdkSandboxAppId, DOES_NOT_EXIST)
+                            == DOES_NOT_EXIST) {
+                        fail("SDK sandbox appId " + sdkSandboxAppId + " does not exist.");
+                    }
+                    if (mAppIdsTrafficPermission.get(sdkSandboxAppId) != permission) {
+                        fail("SDK sandbox appId " + sdkSandboxAppId + " has wrong permission: "
+                                + mAppIdsTrafficPermission.get(sdkSandboxAppId));
+                    }
+                }
             }
         }
     }
@@ -589,6 +637,17 @@
                     if (mUidsNetworkPermission.get(uid) != permission) {
                         fail("uid " + uid + " has wrong permission: " +  permission);
                     }
+                    if (hasSdkSandbox(uid)) {
+                        int sdkSandboxUid = mProcessShim.toSdkSandboxUid(uid);
+                        if (mUidsNetworkPermission.get(sdkSandboxUid, DOES_NOT_EXIST)
+                                == DOES_NOT_EXIST) {
+                            fail("SDK sandbox uid " + uid + " does not exist.");
+                        }
+                        if (mUidsNetworkPermission.get(sdkSandboxUid) != permission) {
+                            fail("SDK sandbox uid " + uid + " has wrong permission: "
+                                    + permission);
+                        }
+                    }
                 }
             }
         }
@@ -600,6 +659,14 @@
                     if (mUidsNetworkPermission.get(uid, DOES_NOT_EXIST) != DOES_NOT_EXIST) {
                         fail("uid " + uid + " has listed permissions, expected none.");
                     }
+                    if (hasSdkSandbox(uid)) {
+                        int sdkSandboxUid = mProcessShim.toSdkSandboxUid(uid);
+                        if (mUidsNetworkPermission.get(sdkSandboxUid, DOES_NOT_EXIST)
+                                != DOES_NOT_EXIST) {
+                            fail("SDK sandbox uid " + sdkSandboxUid
+                                    + " has listed permissions, expected none.");
+                        }
+                    }
                 }
             }
         }
@@ -746,6 +813,7 @@
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11);
+        doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
 
         mPermissionMonitor.startMonitoring();
         final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
@@ -785,9 +853,18 @@
         // MOCK_APPID2: MOCK_PACKAGE2 does not have any permission.
         // SYSTEM_APPID1: SYSTEM_PACKAGE1 has internet permission and update device stats permission
         // SYSTEM_APPID2: SYSTEM_PACKAGE2 has only update device stats permission.
+        // The SDK sandbox APPIDs must have permissions mirroring the app
         SparseIntArray netdPermissionsAppIds = new SparseIntArray();
         netdPermissionsAppIds.put(MOCK_APPID1, PERMISSION_INTERNET);
+        if (hasSdkSandbox(MOCK_APPID1)) {
+            netdPermissionsAppIds.put(mProcessShim.toSdkSandboxUid(MOCK_APPID1),
+                    PERMISSION_INTERNET);
+        }
         netdPermissionsAppIds.put(MOCK_APPID2, PERMISSION_NONE);
+        if (hasSdkSandbox(MOCK_APPID2)) {
+            netdPermissionsAppIds.put(mProcessShim.toSdkSandboxUid(MOCK_APPID2),
+                    PERMISSION_NONE);
+        }
         netdPermissionsAppIds.put(SYSTEM_APPID1, PERMISSION_TRAFFIC_ALL);
         netdPermissionsAppIds.put(SYSTEM_APPID2, PERMISSION_UPDATE_DEVICE_STATS);
 
@@ -827,7 +904,7 @@
         addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
 
-        // Install another package with the same uid and no permissions should not cause the app id
+        // Install another package with the same uid and no permissions should not cause the appId
         // to lose permissions.
         addPackage(MOCK_PACKAGE2, MOCK_UID11);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1195,4 +1272,211 @@
         assertTrue(isHigherNetworkPermission(PERMISSION_SYSTEM, PERMISSION_NETWORK));
         assertFalse(isHigherNetworkPermission(PERMISSION_SYSTEM, PERMISSION_SYSTEM));
     }
+
+    private void prepareMultiUserPackages() {
+        // MOCK_USER1 has installed 3 packages
+        // mockApp1 has no permission and share MOCK_APPID1.
+        // mockApp2 has INTERNET permission and share MOCK_APPID2.
+        // mockApp3 has UPDATE_DEVICE_STATS permission and share MOCK_APPID3.
+        final List<PackageInfo> pkgs1 = List.of(
+                buildPackageInfo("mockApp1", MOCK_UID11),
+                buildPackageInfo("mockApp2", MOCK_UID12, INTERNET),
+                buildPackageInfo("mockApp3", MOCK_UID13, UPDATE_DEVICE_STATS));
+
+        // MOCK_USER2 has installed 2 packages
+        // mockApp4 has UPDATE_DEVICE_STATS permission and share MOCK_APPID1.
+        // mockApp5 has INTERNET permission and share MOCK_APPID2.
+        final List<PackageInfo> pkgs2 = List.of(
+                buildPackageInfo("mockApp4", MOCK_UID21, UPDATE_DEVICE_STATS),
+                buildPackageInfo("mockApp5", MOCK_UID23, INTERNET));
+
+        // MOCK_USER3 has installed 1 packages
+        // mockApp6 has UPDATE_DEVICE_STATS permission and share MOCK_APPID2.
+        final List<PackageInfo> pkgs3 = List.of(
+                buildPackageInfo("mockApp6", MOCK_UID32, UPDATE_DEVICE_STATS));
+
+        doReturn(pkgs1).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
+                eq(MOCK_USER_ID1));
+        doReturn(pkgs2).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
+                eq(MOCK_USER_ID2));
+        doReturn(pkgs3).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
+                eq(MOCK_USER_ID3));
+    }
+
+    private void addUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
+            int appId2Perm, int appId3Perm) {
+        mPermissionMonitor.onUserAdded(user);
+        mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
+        mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
+        mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
+    }
+
+    private void removeUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
+            int appId2Perm, int appId3Perm) {
+        mPermissionMonitor.onUserRemoved(user);
+        mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
+        mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
+        mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
+    }
+
+    @Test
+    public void testAppIdsTrafficPermission_UserAddedRemoved() {
+        prepareMultiUserPackages();
+
+        // Add MOCK_USER1 and verify the permissions with each appIds.
+        addUserAndVerifyAppIdsPermissions(MOCK_USER1, PERMISSION_NONE, PERMISSION_INTERNET,
+                PERMISSION_UPDATE_DEVICE_STATS);
+
+        // Add MOCK_USER2 and verify the permissions upgrade on MOCK_APPID1 & MOCK_APPID3.
+        addUserAndVerifyAppIdsPermissions(MOCK_USER2, PERMISSION_UPDATE_DEVICE_STATS,
+                PERMISSION_INTERNET, PERMISSION_TRAFFIC_ALL);
+
+        // Add MOCK_USER3 and verify the permissions upgrade on MOCK_APPID2.
+        addUserAndVerifyAppIdsPermissions(MOCK_USER3, PERMISSION_UPDATE_DEVICE_STATS,
+                PERMISSION_TRAFFIC_ALL, PERMISSION_TRAFFIC_ALL);
+
+        // Remove MOCK_USER2 and verify the permissions downgrade on MOCK_APPID1 & MOCK_APPID3.
+        removeUserAndVerifyAppIdsPermissions(MOCK_USER2, PERMISSION_NONE, PERMISSION_TRAFFIC_ALL,
+                PERMISSION_UPDATE_DEVICE_STATS);
+
+        // Remove MOCK_USER1 and verify the permissions downgrade on all appIds.
+        removeUserAndVerifyAppIdsPermissions(MOCK_USER1, PERMISSION_UNINSTALLED,
+                PERMISSION_UPDATE_DEVICE_STATS, PERMISSION_UNINSTALLED);
+
+        // Add MOCK_USER2 back and verify the permissions upgrade on MOCK_APPID1 & MOCK_APPID3.
+        addUserAndVerifyAppIdsPermissions(MOCK_USER2, PERMISSION_UPDATE_DEVICE_STATS,
+                PERMISSION_UPDATE_DEVICE_STATS, PERMISSION_INTERNET);
+
+        // Remove MOCK_USER3 and verify the permissions downgrade on MOCK_APPID2.
+        removeUserAndVerifyAppIdsPermissions(MOCK_USER3, PERMISSION_UPDATE_DEVICE_STATS,
+                PERMISSION_UNINSTALLED, PERMISSION_INTERNET);
+    }
+
+    @Test
+    public void testAppIdsTrafficPermission_Multiuser_PackageAdded() throws Exception {
+        // Add two users with empty package list.
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        mPermissionMonitor.onUserAdded(MOCK_USER2);
+
+        final int[] netdPermissions = {PERMISSION_NONE, PERMISSION_INTERNET,
+                PERMISSION_UPDATE_DEVICE_STATS, PERMISSION_TRAFFIC_ALL};
+        final String[][] grantPermissions = {new String[]{}, new String[]{INTERNET},
+                new String[]{UPDATE_DEVICE_STATS}, new String[]{INTERNET, UPDATE_DEVICE_STATS}};
+
+        // Verify that the permission combination is expected when same appId package is installed
+        // on another user. List the expected permissions below.
+        // NONE                + NONE                = NONE
+        // NONE                + INTERNET            = INTERNET
+        // NONE                + UPDATE_DEVICE_STATS = UPDATE_DEVICE_STATS
+        // NONE                + ALL                 = ALL
+        // INTERNET            + NONE                = INTERNET
+        // INTERNET            + INTERNET            = INTERNET
+        // INTERNET            + UPDATE_DEVICE_STATS = ALL
+        // INTERNET            + ALL                 = ALL
+        // UPDATE_DEVICE_STATS + NONE                = UPDATE_DEVICE_STATS
+        // UPDATE_DEVICE_STATS + INTERNET            = ALL
+        // UPDATE_DEVICE_STATS + UPDATE_DEVICE_STATS = UPDATE_DEVICE_STATS
+        // UPDATE_DEVICE_STATS + ALL                 = ALL
+        // ALL                 + NONE                = ALL
+        // ALL                 + INTERNET            = ALL
+        // ALL                 + UPDATE_DEVICE_STATS = ALL
+        // ALL                 + ALL                 = ALL
+        for (int i = 0, num = 0; i < netdPermissions.length; i++) {
+            final int current = netdPermissions[i];
+            final String[] user1Perm = grantPermissions[i];
+            for (int j = 0; j < netdPermissions.length; j++) {
+                final int appId = MOCK_APPID1 + num;
+                final int added = netdPermissions[j];
+                final String[] user2Perm = grantPermissions[j];
+                // Add package on MOCK_USER1 and verify the permission is same as package granted.
+                addPackage(MOCK_PACKAGE1, MOCK_USER1.getUid(appId), user1Perm);
+                mBpfMapMonitor.expectTrafficPerm(current, appId);
+
+                // Add package which share the same appId on MOCK_USER2, and verify the permission
+                // has combined.
+                addPackage(MOCK_PACKAGE2, MOCK_USER2.getUid(appId), user2Perm);
+                mBpfMapMonitor.expectTrafficPerm((current | added), appId);
+                num++;
+            }
+        }
+    }
+
+    private void verifyAppIdPermissionsAfterPackageRemoved(int appId, int expectedPerm,
+            String[] user1Perm, String[] user2Perm) throws Exception {
+        // Add package on MOCK_USER1 and verify the permission is same as package granted.
+        addPackage(MOCK_PACKAGE1, MOCK_USER1.getUid(appId), user1Perm);
+        mBpfMapMonitor.expectTrafficPerm(expectedPerm, appId);
+
+        // Add two packages which share the same appId and don't declare permission on
+        // MOCK_USER2. Verify the permission has no change.
+        addPackage(MOCK_PACKAGE2, MOCK_USER2.getUid(appId));
+        addPackage(MOCK_PACKAGE3, MOCK_USER2.getUid(appId), user2Perm);
+        mBpfMapMonitor.expectTrafficPerm(expectedPerm, appId);
+
+        // Remove one packages from MOCK_USER2. Verify the permission has no change too.
+        removePackage(MOCK_PACKAGE2, MOCK_USER2.getUid(appId));
+        mBpfMapMonitor.expectTrafficPerm(expectedPerm, appId);
+
+        // Remove last packages from MOCK_USER2. Verify the permission has still no change.
+        removePackage(MOCK_PACKAGE3, MOCK_USER2.getUid(appId));
+        mBpfMapMonitor.expectTrafficPerm(expectedPerm, appId);
+    }
+
+    @Test
+    public void testAppIdsTrafficPermission_Multiuser_PackageRemoved() throws Exception {
+        // Add two users with empty package list.
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        mPermissionMonitor.onUserAdded(MOCK_USER2);
+
+        int appId = MOCK_APPID1;
+        // Verify that the permission combination is expected when same appId package is removed on
+        // another user. List the expected permissions below.
+        /***** NONE *****/
+        // NONE + NONE = NONE
+        verifyAppIdPermissionsAfterPackageRemoved(
+                appId++, PERMISSION_NONE, new String[]{}, new String[]{});
+
+        /***** INTERNET *****/
+        // INTERNET + NONE = INTERNET
+        verifyAppIdPermissionsAfterPackageRemoved(
+                appId++, PERMISSION_INTERNET, new String[]{INTERNET}, new String[]{});
+
+        // INTERNET + INTERNET = INTERNET
+        verifyAppIdPermissionsAfterPackageRemoved(
+                appId++, PERMISSION_INTERNET, new String[]{INTERNET}, new String[]{INTERNET});
+
+        /***** UPDATE_DEVICE_STATS *****/
+        // UPDATE_DEVICE_STATS + NONE = UPDATE_DEVICE_STATS
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_UPDATE_DEVICE_STATS,
+                new String[]{UPDATE_DEVICE_STATS}, new String[]{});
+
+        // UPDATE_DEVICE_STATS + UPDATE_DEVICE_STATS = UPDATE_DEVICE_STATS
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_UPDATE_DEVICE_STATS,
+                new String[]{UPDATE_DEVICE_STATS}, new String[]{UPDATE_DEVICE_STATS});
+
+        /***** ALL *****/
+        // ALL + NONE = ALL
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_TRAFFIC_ALL,
+                new String[]{INTERNET, UPDATE_DEVICE_STATS}, new String[]{});
+
+        // ALL + INTERNET = ALL
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_TRAFFIC_ALL,
+                new String[]{INTERNET, UPDATE_DEVICE_STATS}, new String[]{INTERNET});
+
+        // ALL + UPDATE_DEVICE_STATS = ALL
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_TRAFFIC_ALL,
+                new String[]{INTERNET, UPDATE_DEVICE_STATS}, new String[]{UPDATE_DEVICE_STATS});
+
+        // ALL + ALL = ALL
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_TRAFFIC_ALL,
+                new String[]{INTERNET, UPDATE_DEVICE_STATS},
+                new String[]{INTERNET, UPDATE_DEVICE_STATS});
+
+        /***** UNINSTALL *****/
+        // UNINSTALL + UNINSTALL = UNINSTALL
+        verifyAppIdPermissionsAfterPackageRemoved(
+                appId, PERMISSION_NONE, new String[]{}, new String[]{});
+        removePackage(MOCK_PACKAGE1, MOCK_USER1.getUid(appId));
+        mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, appId);
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java
index b8c2673..b8c552e 100644
--- a/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java
+++ b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java
@@ -351,4 +351,55 @@
         expected.add(uids6);
         assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
     }
+
+    @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testConvertArrayToUidRange() {
+        final UidRange uids1_1 = new UidRange(1, 1);
+        final UidRange uids1_2 = new UidRange(1, 2);
+        final UidRange uids100_100 = new UidRange(100, 100);
+        final UidRange uids10_10 = new UidRange(10, 10);
+
+        final UidRange uids10_14 = new UidRange(10, 14);
+        final UidRange uids20_24 = new UidRange(20, 24);
+
+        final Set<UidRange> expected = new ArraySet<>();
+        int[] input = new int[0];
+
+        assertThrows(NullPointerException.class, () -> UidRangeUtils.convertArrayToUidRange(null));
+        assertEquals(expected, UidRangeUtils.convertArrayToUidRange(input));
+
+        input = new int[] {1};
+        expected.add(uids1_1);
+        assertEquals(expected, UidRangeUtils.convertArrayToUidRange(input));
+
+        input = new int[]{1, 2};
+        expected.clear();
+        expected.add(uids1_2);
+        assertEquals(expected, UidRangeUtils.convertArrayToUidRange(input));
+
+        input = new int[]{1, 100};
+        expected.clear();
+        expected.add(uids1_1);
+        expected.add(uids100_100);
+        assertEquals(expected, UidRangeUtils.convertArrayToUidRange(input));
+
+        input = new int[]{100, 1};
+        expected.clear();
+        expected.add(uids1_1);
+        expected.add(uids100_100);
+        assertEquals(expected, UidRangeUtils.convertArrayToUidRange(input));
+
+        input = new int[]{100, 1, 2, 1, 10};
+        expected.clear();
+        expected.add(uids1_2);
+        expected.add(uids10_10);
+        expected.add(uids100_100);
+        assertEquals(expected, UidRangeUtils.convertArrayToUidRange(input));
+
+        input = new int[]{10, 11, 12, 13, 14, 20, 21, 22, 23, 24};
+        expected.clear();
+        expected.add(uids10_14);
+        expected.add(uids20_24);
+        assertEquals(expected, UidRangeUtils.convertArrayToUidRange(input));
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index fd9aefa..33c0868 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -17,6 +17,9 @@
 package com.android.server.connectivity;
 
 import static android.Manifest.permission.BIND_VPN_SERVICE;
+import static android.Manifest.permission.CONTROL_VPN;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 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;
@@ -26,6 +29,9 @@
 import static android.net.INetd.IF_STATE_UP;
 import static android.os.UserHandle.PER_USER_RANGE;
 
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -259,6 +265,10 @@
                         IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME);
         when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any()))
                 .thenReturn(tunnelResp);
+        // The unit test should know what kind of permission it needs and set the permission by
+        // itself, so set the default value of Context#checkCallingOrSelfPermission to
+        // PERMISSION_DENIED.
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
     }
 
     private <T> void mockService(Class<T> clazz, String name, T service) {
@@ -511,6 +521,7 @@
 
     @Test
     public void testLockdownRuleReversibility() throws Exception {
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
         final Vpn vpn = createVpn(primaryUser.id);
         final UidRangeParcel[] entireUser = {
             new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())
@@ -538,6 +549,27 @@
     }
 
     @Test
+    public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
+            throws Exception {
+        assumeTrue(isAtLeastT());
+        final Vpn vpn = createVpnAndSetupUidChecks();
+        assertThrows(SecurityException.class,
+                () -> vpn.prepare("com.not.vpn.owner", null, VpnManager.TYPE_VPN_SERVICE));
+        assertThrows(SecurityException.class,
+                () -> vpn.prepare(null, "com.not.vpn.owner", VpnManager.TYPE_VPN_SERVICE));
+        assertThrows(SecurityException.class,
+                () -> vpn.prepare("com.not.vpn.owner1", "com.not.vpn.owner2",
+                        VpnManager.TYPE_VPN_SERVICE));
+    }
+
+    @Test
+    public void testPrepare_bothOldPackageAndNewPackageAreNull() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+        assertTrue(vpn.prepare(null, null, VpnManager.TYPE_VPN_SERVICE));
+
+    }
+
+    @Test
     public void testIsAlwaysOnPackageSupported() throws Exception {
         final Vpn vpn = createVpn(primaryUser.id);
 
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
new file mode 100644
index 0000000..dfb4fcc
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -0,0 +1,781 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.EthernetNetworkManagementException;
+import android.net.EthernetNetworkSpecifier;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IpConfiguration;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.StaticIpConfiguration;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.InterfaceParams;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetNetworkFactoryTest {
+    private static final int TIMEOUT_MS = 2_000;
+    private static final String TEST_IFACE = "test123";
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private static final String IP_ADDR = "192.0.2.2/25";
+    private static final LinkAddress LINK_ADDR = new LinkAddress(IP_ADDR);
+    private static final String HW_ADDR = "01:02:03:04:05:06";
+    private TestLooper mLooper;
+    private Handler mHandler;
+    private EthernetNetworkFactory mNetFactory = null;
+    private IpClientCallbacks mIpClientCallbacks;
+    @Mock private Context mContext;
+    @Mock private Resources mResources;
+    @Mock private EthernetNetworkFactory.Dependencies mDeps;
+    @Mock private IpClientManager mIpClient;
+    @Mock private EthernetNetworkAgent mNetworkAgent;
+    @Mock private InterfaceParams mInterfaceParams;
+    @Mock private Network mMockNetwork;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        setupNetworkAgentMock();
+        setupIpClientMock();
+        setupContext();
+    }
+
+    //TODO: Move away from usage of TestLooper in order to move this logic back into @Before.
+    private void initEthernetNetworkFactory() {
+        mLooper = new TestLooper();
+        mHandler = new Handler(mLooper.getLooper());
+        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
+    }
+
+    private void setupNetworkAgentMock() {
+        when(mDeps.makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()))
+                .thenAnswer(new AnswerWithArguments() {
+                                       public EthernetNetworkAgent answer(
+                                               Context context,
+                                               Looper looper,
+                                               NetworkCapabilities nc,
+                                               LinkProperties lp,
+                                               NetworkAgentConfig config,
+                                               NetworkProvider provider,
+                                               EthernetNetworkAgent.Callbacks cb) {
+                                           when(mNetworkAgent.getCallbacks()).thenReturn(cb);
+                                           when(mNetworkAgent.getNetwork())
+                                                   .thenReturn(mMockNetwork);
+                                           return mNetworkAgent;
+                                       }
+                                   }
+        );
+    }
+
+    private void setupIpClientMock() throws Exception {
+        doAnswer(inv -> {
+            // these tests only support one concurrent IpClient, so make sure we do not accidentally
+            // create a mess.
+            assertNull("An IpClient has already been created.", mIpClientCallbacks);
+
+            mIpClientCallbacks = inv.getArgument(2);
+            mIpClientCallbacks.onIpClientCreated(null);
+            mLooper.dispatchAll();
+            return null;
+        }).when(mDeps).makeIpClient(any(Context.class), anyString(), any());
+
+        doAnswer(inv -> {
+            mIpClientCallbacks.onQuit();
+            mLooper.dispatchAll();
+            mIpClientCallbacks = null;
+            return null;
+        }).when(mIpClient).shutdown();
+
+        when(mDeps.makeIpClientManager(any())).thenReturn(mIpClient);
+    }
+
+    private void triggerOnProvisioningSuccess() {
+        mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+    }
+
+    private void triggerOnProvisioningFailure() {
+        mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+    }
+
+    private void triggerOnReachabilityLost() {
+        mIpClientCallbacks.onReachabilityLost("ReachabilityLost");
+        mLooper.dispatchAll();
+    }
+
+    private void setupContext() {
+        when(mDeps.getTcpBufferSizesFromResource(eq(mContext))).thenReturn("");
+    }
+
+    @After
+    public void tearDown() {
+        // looper is shared with the network agents, so there may still be messages to dispatch on
+        // tear down.
+        mLooper.dispatchAll();
+    }
+
+    private NetworkCapabilities createDefaultFilterCaps() {
+        return NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .build();
+    }
+
+    private NetworkCapabilities.Builder createInterfaceCapsBuilder(final int transportType) {
+        return new NetworkCapabilities.Builder()
+                .addTransportType(transportType)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+    }
+
+    private NetworkRequest.Builder createDefaultRequestBuilder() {
+        return new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    private NetworkRequest createDefaultRequest() {
+        return createDefaultRequestBuilder().build();
+    }
+
+    private IpConfiguration createDefaultIpConfig() {
+        IpConfiguration ipConfig = new IpConfiguration();
+        ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+        ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE);
+        return ipConfig;
+    }
+
+    /**
+     * Create an {@link IpConfiguration} with an associated {@link StaticIpConfiguration}.
+     *
+     * @return {@link IpConfiguration} with its {@link StaticIpConfiguration} set.
+     */
+    private IpConfiguration createStaticIpConfig() {
+        final IpConfiguration ipConfig = new IpConfiguration();
+        ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC);
+        ipConfig.setStaticIpConfiguration(
+                new StaticIpConfiguration.Builder().setIpAddress(LINK_ADDR).build());
+        return ipConfig;
+    }
+
+    // creates an interface with provisioning in progress (since updating the interface link state
+    // automatically starts the provisioning process)
+    private void createInterfaceUndergoingProvisioning(String iface) {
+        // Default to the ethernet transport type.
+        createInterfaceUndergoingProvisioning(iface, NetworkCapabilities.TRANSPORT_ETHERNET);
+    }
+
+    private void createInterfaceUndergoingProvisioning(
+            @NonNull final String iface, final int transportType) {
+        final IpConfiguration ipConfig = createDefaultIpConfig();
+        mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
+                createInterfaceCapsBuilder(transportType).build());
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+        verifyStart(ipConfig);
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+    }
+
+    // creates a provisioned interface
+    private void createAndVerifyProvisionedInterface(String iface) throws Exception {
+        // Default to the ethernet transport type.
+        createAndVerifyProvisionedInterface(iface, NetworkCapabilities.TRANSPORT_ETHERNET,
+                ConnectivityManager.TYPE_ETHERNET);
+    }
+
+    private void createVerifyAndRemoveProvisionedInterface(final int transportType,
+            final int expectedLegacyType) throws Exception {
+        createAndVerifyProvisionedInterface(TEST_IFACE, transportType,
+                expectedLegacyType);
+        mNetFactory.removeInterface(TEST_IFACE);
+    }
+
+    private void createAndVerifyProvisionedInterface(
+            @NonNull final String iface, final int transportType, final int expectedLegacyType)
+            throws Exception {
+        createInterfaceUndergoingProvisioning(iface, transportType);
+        triggerOnProvisioningSuccess();
+        // provisioning succeeded, verify that the network agent is created, registered, marked
+        // as connected and legacy type are correctly set.
+        final ArgumentCaptor<NetworkCapabilities> ncCaptor = ArgumentCaptor.forClass(
+                NetworkCapabilities.class);
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), ncCaptor.capture(), any(),
+                argThat(x -> x.getLegacyType() == expectedLegacyType), any(), any());
+        assertEquals(
+                new EthernetNetworkSpecifier(iface), ncCaptor.getValue().getNetworkSpecifier());
+        verifyNetworkAgentRegistersAndConnects();
+        clearInvocations(mDeps);
+        clearInvocations(mNetworkAgent);
+    }
+
+    // creates an unprovisioned interface
+    private void createUnprovisionedInterface(String iface) throws Exception {
+        // To create an unprovisioned interface, provision and then "stop" it, i.e. stop its
+        // NetworkAgent and IpClient. One way this can be done is by provisioning an interface and
+        // then calling onNetworkUnwanted.
+        createAndVerifyProvisionedInterface(iface);
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verifyStop();
+
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+    }
+
+    @Test
+    public void testAcceptRequest() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        assertTrue(mNetFactory.acceptRequest(createDefaultRequest()));
+
+        NetworkRequest wifiRequest = createDefaultRequestBuilder()
+                .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
+        assertFalse(mNetFactory.acceptRequest(wifiRequest));
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        // verify that the IpClient gets shut down when interface state changes to down.
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        verify(mIpClient).shutdown();
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        verifyStop();
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        // There should not be an active IPClient or NetworkAgent.
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+        verify(mDeps, never())
+                .makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        // if interface was never added, link state cannot be updated.
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+
+        assertFalse(ret);
+        verifyNoStopOrStart();
+        listener.expectOnError();
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateWithNoChanges() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+
+        assertFalse(ret);
+        verifyNoStopOrStart();
+        listener.expectOnError();
+    }
+
+    @Test
+    public void testNeedNetworkForOnProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient).startProvisioning(any());
+
+        triggerOnProvisioningSuccess();
+        verifyNetworkAgentRegistersAndConnects();
+    }
+
+    @Test
+    public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+
+        triggerOnProvisioningSuccess();
+        verifyNetworkAgentRegistersAndConnects();
+    }
+
+    @Test
+    public void testProvisioningLoss() throws Exception {
+        initEthernetNetworkFactory();
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        triggerOnProvisioningFailure();
+        verifyStop();
+        // provisioning loss should trigger a retry, since the interface is still there
+        verify(mIpClient).startProvisioning(any());
+    }
+
+    @Test
+    public void testProvisioningLossForDisappearedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        // mocked method returns null by default, but just to be explicit in the test:
+        when(mDeps.getNetworkInterfaceByName(eq(TEST_IFACE))).thenReturn(null);
+
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        triggerOnProvisioningFailure();
+
+        // the interface disappeared and getNetworkInterfaceByName returns null, we should not retry
+        verify(mIpClient, never()).startProvisioning(any());
+        verifyNoStopOrStart();
+    }
+
+    private void verifyNoStopOrStart() {
+        verify(mNetworkAgent, never()).register();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER);
+
+        mNetFactory.needNetworkFor(createDefaultRequest());
+
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+
+        // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should
+        // not start an IpClient when the link is down, but fixing this may make matters worse by
+        // tiggering b/197548738.
+        NetworkRequest specificNetRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
+                .build();
+        mNetFactory.needNetworkFor(specificNetRequest);
+        mNetFactory.releaseNetworkFor(specificNetRequest);
+
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER);
+        // TODO: change to once when b/191854824 is fixed.
+        verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any());
+    }
+
+    @Test
+    public void testLinkPropertiesChanged() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        LinkProperties lp = new LinkProperties();
+        mIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+        verify(mNetworkAgent).sendLinkPropertiesImpl(same(lp));
+    }
+
+    @Test
+    public void testNetworkUnwanted() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verifyStop();
+    }
+
+    @Test
+    public void testNetworkUnwantedWithStaleNetworkAgent() throws Exception {
+        initEthernetNetworkFactory();
+        // ensures provisioning is restarted after provisioning loss
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        EthernetNetworkAgent.Callbacks oldCbs = mNetworkAgent.getCallbacks();
+        // replace network agent in EthernetNetworkFactory
+        // Loss of provisioning will restart the ip client and network agent.
+        triggerOnProvisioningFailure();
+        verify(mDeps).makeIpClient(any(), any(), any());
+
+        triggerOnProvisioningSuccess();
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
+
+        // verify that unwanted is ignored
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+        oldCbs.onNetworkUnwanted();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+    }
+
+    @Test
+    public void testTransportOverrideIsCorrectlySet() throws Exception {
+        initEthernetNetworkFactory();
+        // createProvisionedInterface() has verifications in place for transport override
+        // functionality which for EthernetNetworkFactory is network score and legacy type mappings.
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_ETHERNET,
+                ConnectivityManager.TYPE_ETHERNET);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_BLUETOOTH,
+                ConnectivityManager.TYPE_BLUETOOTH);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI,
+                ConnectivityManager.TYPE_WIFI);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_CELLULAR,
+                ConnectivityManager.TYPE_MOBILE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_LOWPAN,
+                ConnectivityManager.TYPE_NONE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+                ConnectivityManager.TYPE_NONE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_TEST,
+                ConnectivityManager.TYPE_NONE);
+    }
+
+    @Test
+    public void testReachabilityLoss() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        triggerOnReachabilityLost();
+
+        // Reachability loss should trigger a stop and start, since the interface is still there
+        verifyRestart(createDefaultIpConfig());
+    }
+
+    private IpClientCallbacks getStaleIpClientCallbacks() throws Exception {
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final IpClientCallbacks staleIpClientCallbacks = mIpClientCallbacks;
+        mNetFactory.removeInterface(TEST_IFACE);
+        verifyStop();
+        assertNotSame(mIpClientCallbacks, staleIpClientCallbacks);
+        return staleIpClientCallbacks;
+    }
+
+    @Test
+    public void testIgnoreOnIpLayerStartedCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+    }
+
+    @Test
+    public void testIgnoreOnIpLayerStoppedCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIgnoreLinkPropertiesCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+        final LinkProperties lp = new LinkProperties();
+
+        staleIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+
+        verify(mNetworkAgent, never()).sendLinkPropertiesImpl(eq(lp));
+    }
+
+    @Test
+    public void testIgnoreNeighborLossCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onReachabilityLost("Neighbor Lost");
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+    }
+
+    private void verifyRestart(@NonNull final IpConfiguration ipConfig) {
+        verifyStop();
+        verifyStart(ipConfig);
+    }
+
+    private void verifyStart(@NonNull final IpConfiguration ipConfig) {
+        verify(mDeps).makeIpClient(any(Context.class), anyString(), any());
+        verify(mIpClient).startProvisioning(
+                argThat(x -> Objects.equals(x.mStaticIpConfig, ipConfig.getStaticIpConfiguration()))
+        );
+    }
+
+    private void verifyStop() {
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+    }
+
+    private void verifyNetworkAgentRegistersAndConnects() {
+        verify(mNetworkAgent).register();
+        verify(mNetworkAgent).markConnected();
+    }
+
+    private static final class TestNetworkManagementListener
+            implements INetworkInterfaceOutcomeReceiver {
+        private final CompletableFuture<String> mResult = new CompletableFuture<>();
+
+        @Override
+        public void onResult(@NonNull String iface) {
+            mResult.complete(iface);
+        }
+
+        @Override
+        public void onError(@NonNull EthernetNetworkManagementException exception) {
+            mResult.completeExceptionally(exception);
+        }
+
+        String expectOnResult() throws Exception {
+            return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        void expectOnError() throws Exception {
+            assertThrows(EthernetNetworkManagementException.class, () -> {
+                try {
+                    mResult.get();
+                } catch (ExecutionException e) {
+                    throw e.getCause();
+                }
+            });
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+    }
+
+    @Test
+    public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        triggerOnProvisioningSuccess();
+
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.removeInterface(TEST_IFACE));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
+        initEthernetNetworkFactory();
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener successfulListener =
+                new TestNetworkManagementListener();
+
+        // If two calls come in before the first one completes, the first listener will be aborted
+        // and the second one will be successful.
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> {
+                    mNetFactory.updateInterface(
+                            TEST_IFACE, ipConfiguration, capabilities, successfulListener);
+                    triggerOnProvisioningSuccess();
+                });
+
+        assertEquals(successfulListener.expectOnResult(), TEST_IFACE);
+    }
+
+    private void verifyNetworkManagementCallIsAbortedWhenInterrupted(
+            @NonNull final String iface,
+            @NonNull final Runnable interruptingRunnable) throws Exception {
+        createAndVerifyProvisionedInterface(iface);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener failedListener = new TestNetworkManagementListener();
+
+        // An active update request will be aborted on interrupt prior to provisioning completion.
+        mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener);
+        interruptingRunnable.run();
+
+        failedListener.expectOnError();
+    }
+
+    @Test
+    public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        triggerOnProvisioningSuccess();
+
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(),
+                eq(capabilities), any(), any(), any(), any());
+        verifyRestart(ipConfiguration);
+    }
+
+    @Test
+    public void testUpdateInterfaceForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
+        // No interface exists due to not calling createAndVerifyProvisionedInterface(...).
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+
+        verifyNoStopOrStart();
+        listener.expectOnError();
+    }
+
+    @Test
+    public void testUpdateInterfaceWithNullIpConfiguration() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        final IpConfiguration initialIpConfig = createStaticIpConfig();
+        mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/,
+                null /*listener*/);
+        triggerOnProvisioningSuccess();
+        verifyRestart(initialIpConfig);
+
+        // TODO: have verifyXyz functions clear invocations.
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+
+
+        // verify that sending a null ipConfig does not update the current ipConfig.
+        mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/,
+                null /*listener*/);
+        triggerOnProvisioningSuccess();
+        verifyRestart(initialIpConfig);
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
new file mode 100644
index 0000000..dd1f1ed
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.os.Handler;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetServiceImplTest {
+    private static final String TEST_IFACE = "test123";
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(new IpConfiguration())
+                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .build();
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(new IpConfiguration())
+                    .build();
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .build();
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private EthernetServiceImpl mEthernetServiceImpl;
+    @Mock private Context mContext;
+    @Mock private Handler mHandler;
+    @Mock private EthernetTracker mEthernetTracker;
+    @Mock private PackageManager mPackageManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        mEthernetServiceImpl = new EthernetServiceImpl(mContext, mHandler, mEthernetTracker);
+        mEthernetServiceImpl.mStarted.set(true);
+        toggleAutomotiveFeature(true);
+        shouldTrackIface(TEST_IFACE, true);
+    }
+
+    private void toggleAutomotiveFeature(final boolean isEnabled) {
+        doReturn(isEnabled)
+                .when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    private void shouldTrackIface(@NonNull final String iface, final boolean shouldTrack) {
+        doReturn(shouldTrack).when(mEthernetTracker).isTrackingInterface(iface);
+    }
+
+    @Test
+    public void testSetConfigurationRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.setConfiguration("" /* iface */, new IpConfiguration());
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(
+                    "" /* iface */, UPDATE_REQUEST, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(null, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationWithCapabilitiesRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES,
+                NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getIpConfiguration()),
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER);
+        });
+    }
+
+    private void denyManageEthPermission() {
+        doThrow(new SecurityException("")).when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(Manifest.permission.MANAGE_ETHERNET_NETWORKS), anyString());
+    }
+
+    private void denyManageTestNetworksPermission() {
+        doThrow(new SecurityException("")).when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(Manifest.permission.MANAGE_TEST_NETWORKS), anyString());
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    private void enableTestInterface() {
+        when(mEthernetTracker.isValidTestInterface(eq(TEST_IFACE))).thenReturn(true);
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfiguration() {
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                eq(UPDATE_REQUEST.getIpConfiguration()),
+                eq(UPDATE_REQUEST.getNetworkCapabilities()), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testConnectNetwork() {
+        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetwork() {
+        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testUpdateConfigurationAcceptsTestRequestWithNullCapabilities() {
+        enableTestInterface();
+        final EthernetNetworkUpdateRequest request =
+                new EthernetNetworkUpdateRequest
+                        .Builder()
+                        .setIpConfiguration(new IpConfiguration()).build();
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(request.getIpConfiguration()),
+                eq(request.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testUpdateConfigurationAcceptsRequestWithNullIpConfiguration() {
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_IP_CONFIG,
+                NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getIpConfiguration()),
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsInvalidTestRequest() {
+        enableTestInterface();
+        assertThrows(IllegalArgumentException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    private EthernetNetworkUpdateRequest createTestNetworkUpdateRequest() {
+        final NetworkCapabilities nc =  new NetworkCapabilities
+                .Builder(UPDATE_REQUEST.getNetworkCapabilities())
+                .addTransportType(TRANSPORT_TEST).build();
+
+        return new EthernetNetworkUpdateRequest
+                .Builder(UPDATE_REQUEST)
+                .setNetworkCapabilities(nc).build();
+    }
+
+    @Test
+    public void testUpdateConfigurationForTestRequestDoesNotRequireAutoOrEthernetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+        final EthernetNetworkUpdateRequest request = createTestNetworkUpdateRequest();
+
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                eq(request.getIpConfiguration()),
+                eq(request.getNetworkCapabilities()), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    private void denyPermissions(String... permissions) {
+        for (String permission: permissions) {
+            doReturn(PackageManager.PERMISSION_DENIED).when(mContext)
+                    .checkCallingOrSelfPermission(eq(permission));
+        }
+    }
+
+    @Test
+    public void testSetEthernetEnabled() {
+        denyPermissions(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+        mEthernetServiceImpl.setEthernetEnabled(true);
+        verify(mEthernetTracker).setEthernetEnabled(true);
+        reset(mEthernetTracker);
+
+        denyPermissions(Manifest.permission.NETWORK_STACK);
+        mEthernetServiceImpl.setEthernetEnabled(false);
+        verify(mEthernetTracker).setEthernetEnabled(false);
+        reset(mEthernetTracker);
+
+        denyPermissions(Manifest.permission.NETWORK_SETTINGS);
+        try {
+            mEthernetServiceImpl.setEthernetEnabled(true);
+            fail("Should get SecurityException");
+        } catch (SecurityException e) { }
+        verify(mEthernetTracker, never()).setEthernetEnabled(false);
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
new file mode 100644
index 0000000..b1831c4
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.EthernetManager;
+import android.net.InetAddresses;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IEthernetServiceListener;
+import android.net.INetd;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+import android.os.HandlerThread;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EthernetTrackerTest {
+    private static final String TEST_IFACE = "test123";
+    private static final int TIMEOUT_MS = 1_000;
+    private static final String THREAD_NAME = "EthernetServiceThread";
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private EthernetTracker tracker;
+    private HandlerThread mHandlerThread;
+    @Mock private Context mContext;
+    @Mock private EthernetNetworkFactory mFactory;
+    @Mock private INetd mNetd;
+    @Mock private EthernetTracker.Dependencies mDeps;
+
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+        initMockResources();
+        when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean(), any())).thenReturn(false);
+        when(mNetd.interfaceGetList()).thenReturn(new String[0]);
+        mHandlerThread = new HandlerThread(THREAD_NAME);
+        mHandlerThread.start();
+        tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
+                mDeps);
+    }
+
+    @After
+    public void cleanUp() {
+        mHandlerThread.quitSafely();
+    }
+
+    private void initMockResources() {
+        when(mDeps.getInterfaceRegexFromResource(eq(mContext))).thenReturn("");
+        when(mDeps.getInterfaceConfigFromResource(eq(mContext))).thenReturn(new String[0]);
+    }
+
+    private void waitForIdle() {
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+    }
+
+    /**
+     * Test: Creation of various valid static IP configurations
+     */
+    @Test
+    public void createStaticIpConfiguration() {
+        // Empty gives default StaticIPConfiguration object
+        assertStaticConfiguration(new StaticIpConfiguration(), "");
+
+        // Setting only the IP address properly cascades and assumes defaults
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24")).build(), "ip=192.0.2.10/24");
+
+        final ArrayList<InetAddress> dnsAddresses = new ArrayList<>();
+        dnsAddresses.add(InetAddresses.parseNumericAddress("4.4.4.4"));
+        dnsAddresses.add(InetAddresses.parseNumericAddress("8.8.8.8"));
+        // Setting other fields properly cascades them
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24"))
+                .setDnsServers(dnsAddresses)
+                .setGateway(InetAddresses.parseNumericAddress("192.0.2.1"))
+                .setDomains("android").build(),
+                "ip=192.0.2.10/24 dns=4.4.4.4,8.8.8.8 gateway=192.0.2.1 domains=android");
+
+        // Verify order doesn't matter
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24"))
+                .setDnsServers(dnsAddresses)
+                .setGateway(InetAddresses.parseNumericAddress("192.0.2.1"))
+                .setDomains("android").build(),
+                "domains=android ip=192.0.2.10/24 gateway=192.0.2.1 dns=4.4.4.4,8.8.8.8 ");
+    }
+
+    /**
+     * Test: Attempt creation of various bad static IP configurations
+     */
+    @Test
+    public void createStaticIpConfiguration_Bad() {
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway= blah=20.20.20.20");  // Unknown key
+        assertStaticConfigurationFails("ip=192.0.2.1");  // mask is missing
+        assertStaticConfigurationFails("ip=a.b.c");  // not a valid ip address
+        assertStaticConfigurationFails("dns=4.4.4.4,1.2.3.A");  // not valid ip address in dns
+        assertStaticConfigurationFails("=");  // Key and value is empty
+        assertStaticConfigurationFails("ip=");  // Value is empty
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway=");  // Gateway is empty
+    }
+
+    private void assertStaticConfigurationFails(String config) {
+        try {
+            EthernetTracker.parseStaticIpConfiguration(config);
+            fail("Expected to fail: " + config);
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    private void assertStaticConfiguration(StaticIpConfiguration expectedStaticIpConfig,
+                String configAsString) {
+        final IpConfiguration expectedIpConfiguration = new IpConfiguration();
+        expectedIpConfiguration.setIpAssignment(IpAssignment.STATIC);
+        expectedIpConfiguration.setProxySettings(ProxySettings.NONE);
+        expectedIpConfiguration.setStaticIpConfiguration(expectedStaticIpConfig);
+
+        assertEquals(expectedIpConfiguration,
+                EthernetTracker.parseStaticIpConfiguration(configAsString));
+    }
+
+    private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) {
+        final NetworkCapabilities.Builder builder =
+                clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                        : new NetworkCapabilities.Builder();
+        return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+    }
+
+    /**
+     * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+     */
+    @Test
+    public void createNetworkCapabilities() {
+
+        // Particularly common expected results
+        NetworkCapabilities defaultEthernetCleared =
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build();
+
+        NetworkCapabilities ethernetClearedWithCommonCaps =
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .build();
+
+        // Empty capabilities and transports lists with a "please clear defaults" should
+        // yield an empty capabilities set with TRANPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "");
+
+        // Empty capabilities and transports without the clear defaults flag should return the
+        // default capabilities set with TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(false /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build(),
+                false, "", "");
+
+        // A list of capabilities without the clear defaults flag should return the default
+        // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(false /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(11)
+                        .addCapability(12)
+                        .build(),
+                false, "11,12", "");
+
+        // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
+        // with a default TRANSPORT_ETHERNET since no overrides are specified
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+
+        // Adding any invalid capabilities to the list will cause them to be ignored
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+
+        // Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
+        // and apply only the override to the capabiltities object
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(0)
+                        .build(),
+                true, "", "0");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(1)
+                        .build(),
+                true, "", "1");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(2)
+                        .build(),
+                true, "", "2");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(3)
+                        .build(),
+                true, "", "3");
+
+        // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+
+        // "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+
+        // "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+
+        // Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100");
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+
+        // Ensure the adding of both capabilities and transports work
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .addTransportType(3)
+                        .build(),
+                true, "12,13,14,15", "3");
+
+        // Ensure order does not matter for capability list
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+    }
+
+    private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
+            boolean clearCapabilties, String configCapabiltiies,String configTransports) {
+        assertEquals(expectedNetworkCapabilities,
+                EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
+                        configTransports).build());
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfigReturnsCorrectValue() {
+        final String capabilities = "2";
+        final String ipConfig = "3";
+        final String transport = "4";
+        final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport);
+
+        final EthernetTracker.EthernetTrackerConfig config =
+                EthernetTracker.createEthernetTrackerConfig(configString);
+
+        assertEquals(TEST_IFACE, config.mIface);
+        assertEquals(capabilities, config.mCapabilities);
+        assertEquals(ipConfig, config.mIpConfig);
+        assertEquals(transport, config.mTransport);
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfigThrowsNpeWithNullInput() {
+        assertThrows(NullPointerException.class,
+                () -> EthernetTracker.createEthernetTrackerConfig(null));
+    }
+
+    @Test
+    public void testUpdateConfiguration() {
+        final NetworkCapabilities capabilities = new NetworkCapabilities.Builder().build();
+        final LinkAddress linkAddr = new LinkAddress("192.0.2.2/25");
+        final StaticIpConfiguration staticIpConfig =
+                new StaticIpConfiguration.Builder().setIpAddress(linkAddr).build();
+        final IpConfiguration ipConfig =
+                new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
+        final INetworkInterfaceOutcomeReceiver listener = null;
+
+        tracker.updateConfiguration(TEST_IFACE, ipConfig, capabilities, listener);
+        waitForIdle();
+
+        verify(mFactory).updateInterface(
+                eq(TEST_IFACE), eq(ipConfig), eq(capabilities), eq(listener));
+    }
+
+    @Test
+    public void testConnectNetworkCorrectlyCallsFactory() {
+        tracker.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetworkCorrectlyCallsFactory() {
+        tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsFalseWhenTestInterfacesAreNotIncluded() {
+        final String validIfaceName = TEST_TAP_PREFIX + "123";
+        tracker.setIncludeTestInterfaces(false);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName);
+
+        assertFalse(isValidTestInterface);
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsFalseWhenTestInterfaceNameIsInvalid() {
+        final String invalidIfaceName = "123" + TEST_TAP_PREFIX;
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(invalidIfaceName);
+
+        assertFalse(isValidTestInterface);
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsTrueWhenTestInterfacesIncludedAndValidName() {
+        final String validIfaceName = TEST_TAP_PREFIX + "123";
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName);
+
+        assertTrue(isValidTestInterface);
+    }
+
+    public static class EthernetStateListener extends IEthernetServiceListener.Stub {
+        @Override
+        public void onEthernetStateChanged(int state) { }
+
+        @Override
+        public void onInterfaceStateChanged(String iface, int state, int role,
+                IpConfiguration configuration) { }
+    }
+
+    @Test
+    public void testListenEthernetStateChange() throws Exception {
+        final String testIface = "testtap123";
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
+        ifaceParcel.ifName = testIface;
+        ifaceParcel.hwAddr = testHwAddr;
+        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
+
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
+        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+
+        final EthernetStateListener listener = spy(new EthernetStateListener());
+        tracker.addListener(listener, true /* canUseRestrictedNetworks */);
+        // Check default state.
+        waitForIdle();
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
+                anyInt(), any());
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
+        reset(listener);
+
+        doReturn(EthernetManager.STATE_ABSENT).when(mFactory).getInterfaceState(eq(testIface));
+        tracker.setEthernetEnabled(false);
+        waitForIdle();
+        verify(mFactory).removeInterface(eq(testIface));
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_DISABLED));
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_ABSENT),
+                anyInt(), any());
+        reset(listener);
+
+        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+        tracker.setEthernetEnabled(true);
+        waitForIdle();
+        verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
+                anyInt(), any());
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/IpConfigStoreTest.java b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
new file mode 100644
index 0000000..e9a5309
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+import android.util.ArrayMap;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link IpConfigStore}
+ */
+@RunWith(AndroidJUnit4.class)
+public class IpConfigStoreTest {
+    private static final int KEY_CONFIG = 17;
+    private static final String IFACE_1 = "eth0";
+    private static final String IFACE_2 = "eth1";
+    private static final String IP_ADDR_1 = "192.168.1.10/24";
+    private static final String IP_ADDR_2 = "192.168.1.20/24";
+    private static final String DNS_IP_ADDR_1 = "1.2.3.4";
+    private static final String DNS_IP_ADDR_2 = "5.6.7.8";
+
+    @Test
+    public void backwardCompatibility2to3() throws IOException {
+        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+        DataOutputStream outputStream = new DataOutputStream(byteStream);
+
+        final IpConfiguration expectedConfig =
+                newIpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null);
+
+        // Emulate writing to old format.
+        writeDhcpConfigV2(outputStream, KEY_CONFIG, expectedConfig);
+
+        InputStream in = new ByteArrayInputStream(byteStream.toByteArray());
+        ArrayMap<String, IpConfiguration> configurations = IpConfigStore.readIpConfigurations(in);
+
+        assertNotNull(configurations);
+        assertEquals(1, configurations.size());
+        IpConfiguration actualConfig = configurations.get(String.valueOf(KEY_CONFIG));
+        assertNotNull(actualConfig);
+        assertEquals(expectedConfig, actualConfig);
+    }
+
+    @Test
+    public void staticIpMultiNetworks() throws Exception {
+        final ArrayList<InetAddress> dnsServers = new ArrayList<>();
+        dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_1));
+        dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_2));
+        final StaticIpConfiguration staticIpConfiguration1 = new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress(IP_ADDR_1))
+                .setDnsServers(dnsServers).build();
+        final StaticIpConfiguration staticIpConfiguration2 = new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress(IP_ADDR_2))
+                .setDnsServers(dnsServers).build();
+
+        ProxyInfo proxyInfo =
+                ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2"));
+
+        IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, staticIpConfiguration1, proxyInfo);
+        IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, staticIpConfiguration2, proxyInfo);
+
+        ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+        expectedNetworks.put(IFACE_1, expectedConfig1);
+        expectedNetworks.put(IFACE_2, expectedConfig2);
+
+        MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
+        IpConfigStore store = new IpConfigStore(writer);
+        store.writeIpConfigurations("file/path/not/used/", expectedNetworks);
+
+        InputStream in = new ByteArrayInputStream(writer.mByteStream.toByteArray());
+        ArrayMap<String, IpConfiguration> actualNetworks = IpConfigStore.readIpConfigurations(in);
+        assertNotNull(actualNetworks);
+        assertEquals(2, actualNetworks.size());
+        assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
+        assertEquals(expectedNetworks.get(IFACE_2), actualNetworks.get(IFACE_2));
+    }
+
+    private IpConfiguration newIpConfiguration(IpAssignment ipAssignment,
+            ProxySettings proxySettings, StaticIpConfiguration staticIpConfig, ProxyInfo info) {
+        final IpConfiguration config = new IpConfiguration();
+        config.setIpAssignment(ipAssignment);
+        config.setProxySettings(proxySettings);
+        config.setStaticIpConfiguration(staticIpConfig);
+        config.setHttpProxy(info);
+        return config;
+    }
+
+    // This is simplified snapshot of code that was used to store values in V2 format (key as int).
+    private static void writeDhcpConfigV2(DataOutputStream out, int configKey,
+            IpConfiguration config) throws IOException {
+        out.writeInt(2);  // VERSION 2
+        switch (config.getIpAssignment()) {
+            case DHCP:
+                out.writeUTF("ipAssignment");
+                out.writeUTF(config.getIpAssignment().toString());
+                break;
+            default:
+                fail("Not supported in test environment");
+        }
+
+        out.writeUTF("id");
+        out.writeInt(configKey);
+        out.writeUTF("eos");
+    }
+
+    /** Synchronously writes into given byte steam */
+    private static class MockedDelayedDiskWrite extends DelayedDiskWrite {
+        final ByteArrayOutputStream mByteStream = new ByteArrayOutputStream();
+
+        @Override
+        public void write(String filePath, Writer w) {
+            DataOutputStream outputStream = new DataOutputStream(mByteStream);
+
+            try {
+                w.onWriteCalled(outputStream);
+            } catch (IOException e) {
+                fail();
+            }
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 6872f80..79744b1 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -32,12 +32,9 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doReturn;
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.net.ConnectivityManager;
 import android.net.NetworkStats;
 import android.net.TrafficStats;
 import android.net.UnderlyingNetworkInfo;
@@ -77,7 +74,6 @@
     private File mTestProc;
     private NetworkStatsFactory mFactory;
     @Mock private Context mContext;
-    @Mock private ConnectivityManager mCm;
 
     @Before
     public void setUp() throws Exception {
@@ -88,9 +84,6 @@
         // applications. So in order to have a test support native library, the native code
         // related to networkStatsFactory is compiled to a minimal native library and loaded here.
         System.loadLibrary("networkstatsfactorytestjni");
-        doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
-                eq(ConnectivityManager.class));
-        doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
         mFactory = new NetworkStatsFactory(mContext, mTestProc, false);
         mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
     }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 66dcf6d..5f9d1ff 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 
+import android.content.Context;
 import android.net.DataUsageRequest;
 import android.net.NetworkIdentity;
 import android.net.NetworkIdentitySet;
@@ -78,6 +79,7 @@
 
     private static final String IMSI_1 = "310004";
     private static final String IMSI_2 = "310260";
+    private static final int SUBID_1 = 1;
     private static final String TEST_SSID = "AndroidAP";
 
     private static NetworkTemplate sTemplateWifi = buildTemplateWifiWildcard();
@@ -101,6 +103,7 @@
 
     @Mock private IBinder mUsageCallbackBinder;
     private TestableUsageCallback mUsageCallback;
+    @Mock private Context mContext;
 
     @Before
     public void setUp() throws Exception {
@@ -127,14 +130,14 @@
         final DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
 
-        final DataUsageRequest requestByApp = mStatsObservers.register(inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.DEVICE);
+        final DataUsageRequest requestByApp = mStatsObservers.register(mContext, inputRequest,
+                mUsageCallback, UID_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(requestByApp.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, requestByApp.template));
-        assertEquals(THRESHOLD_BYTES, requestByApp.thresholdInBytes);
+        assertEquals(thresholdTooLowBytes, requestByApp.thresholdInBytes);
 
         // Verify the threshold requested by system uid won't be overridden.
-        final DataUsageRequest requestBySystem = mStatsObservers.register(inputRequest,
+        final DataUsageRequest requestBySystem = mStatsObservers.register(mContext, inputRequest,
                 mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(requestBySystem.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template));
@@ -147,7 +150,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
@@ -159,13 +162,13 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
 
-        DataUsageRequest request1 = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request1 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request1.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request1.template));
         assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
 
-        DataUsageRequest request2 = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request2 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request2.requestId > request1.requestId);
         assertTrue(Objects.equals(sTemplateWifi, request2.template));
@@ -185,7 +188,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -205,7 +208,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -224,7 +227,7 @@
         identSet.add(new NetworkIdentity(
                 TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN,
                 IMSI_1, null /* networkId */, false /* roaming */, true /* metered */,
-                true /* defaultNetwork */, OEM_NONE));
+                true /* defaultNetwork */, OEM_NONE, SUBID_1));
         return identSet;
     }
 
@@ -233,7 +236,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -257,7 +260,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -287,7 +290,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -318,7 +321,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -351,7 +354,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -383,7 +386,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 UID_BLUE, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -416,7 +419,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
+        DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 76c0c38..ceeb997 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -63,8 +63,10 @@
 
 import static com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -77,6 +79,7 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -101,13 +104,13 @@
 import android.net.UnderlyingNetworkInfo;
 import android.net.netstats.provider.INetworkStatsProviderCallback;
 import android.net.wifi.WifiInfo;
-import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.SimpleClock;
 import android.provider.Settings;
+import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
 
 import androidx.annotation.Nullable;
@@ -125,6 +128,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
+import com.android.testutils.TestBpfMap;
 import com.android.testutils.TestableNetworkStatsProviderBinder;
 
 import libcore.testing.io.TestIoUtils;
@@ -143,6 +147,7 @@
 import java.time.ZoneOffset;
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Tests for {@link NetworkStatsService}.
@@ -152,7 +157,8 @@
  */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+// NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
     private static final String TAG = "NetworkStatsServiceTest";
 
@@ -197,8 +203,16 @@
     private HandlerThread mHandlerThread;
     @Mock
     private LocationPermissionChecker mLocationPermissionChecker;
-    private @Mock IBpfMap<U32, U8> mUidCounterSetMap;
-    private @Mock NetworkStatsService.TagStatsDeleter mTagStatsDeleter;
+    private TestBpfMap<U32, U8> mUidCounterSetMap = spy(new TestBpfMap<>(U32.class, U8.class));
+
+    private TestBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap = new TestBpfMap<>(
+            CookieTagMapKey.class, CookieTagMapValue.class);
+    private TestBpfMap<StatsMapKey, StatsMapValue> mStatsMapA = new TestBpfMap<>(StatsMapKey.class,
+            StatsMapValue.class);
+    private TestBpfMap<StatsMapKey, StatsMapValue> mStatsMapB = new TestBpfMap<>(StatsMapKey.class,
+            StatsMapValue.class);
+    private TestBpfMap<UidStatsMapKey, StatsMapValue> mAppUidStatsMap = new TestBpfMap<>(
+            UidStatsMapKey.class, StatsMapValue.class);
 
     private NetworkStatsService mService;
     private INetworkStatsSession mSession;
@@ -361,8 +375,23 @@
             }
 
             @Override
-            public NetworkStatsService.TagStatsDeleter getTagStatsDeleter() {
-                return mTagStatsDeleter;
+            public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
+                return mCookieTagMap;
+            }
+
+            @Override
+            public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
+                return mStatsMapA;
+            }
+
+            @Override
+            public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
+                return mStatsMapB;
+            }
+
+            @Override
+            public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
+                return mAppUidStatsMap;
             }
         };
     }
@@ -488,10 +517,10 @@
                 .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
-        mService.setUidForeground(UID_RED, false);
+        mService.noteUidForeground(UID_RED, false);
         verify(mUidCounterSetMap, never()).deleteEntry(any());
         mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
-        mService.setUidForeground(UID_RED, true);
+        mService.noteUidForeground(UID_RED, true);
         verify(mUidCounterSetMap).updateEntry(
                 eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
         mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
@@ -702,10 +731,8 @@
         final Intent intent = new Intent(ACTION_UID_REMOVED);
         intent.putExtra(EXTRA_UID, UID_BLUE);
         mServiceContext.sendBroadcast(intent);
-        verify(mTagStatsDeleter).deleteTagData(UID_BLUE);
         intent.putExtra(EXTRA_UID, UID_RED);
         mServiceContext.sendBroadcast(intent);
-        verify(mTagStatsDeleter).deleteTagData(UID_RED);
 
         // existing uid and total should remain unchanged; but removed UID
         // should be gone completely.
@@ -1091,7 +1118,7 @@
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L));
-        mService.setUidForeground(UID_RED, true);
+        mService.noteUidForeground(UID_RED, true);
         verify(mUidCounterSetMap).updateEntry(
                 eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
         mService.incrementOperationCount(UID_RED, 0xFAAD, 1);
@@ -1905,4 +1932,70 @@
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
     }
+
+    private boolean cookieTagMapContainsUid(int uid) throws ErrnoException {
+        final AtomicBoolean found = new AtomicBoolean();
+        mCookieTagMap.forEach((k, v) -> {
+            if (v.uid == uid) {
+                found.set(true);
+            }
+        });
+        return found.get();
+    }
+
+    private static <K extends StatsMapKey, V extends StatsMapValue> boolean statsMapContainsUid(
+            TestBpfMap<K, V> map, int uid) throws ErrnoException {
+        final AtomicBoolean found = new AtomicBoolean();
+        map.forEach((k, v) -> {
+            if (k.uid == uid) {
+                found.set(true);
+            }
+        });
+        return found.get();
+    }
+
+    private void initBpfMapsWithTagData(int uid) throws ErrnoException {
+        // key needs to be unique, use some offset from uid.
+        mCookieTagMap.insertEntry(new CookieTagMapKey(1000 + uid), new CookieTagMapValue(uid, 1));
+        mCookieTagMap.insertEntry(new CookieTagMapKey(2000 + uid), new CookieTagMapValue(uid, 2));
+
+        mStatsMapA.insertEntry(new StatsMapKey(uid, 1, 0, 10), new StatsMapValue(5, 5000, 3, 3000));
+        mStatsMapA.insertEntry(new StatsMapKey(uid, 2, 0, 10), new StatsMapValue(5, 5000, 3, 3000));
+
+        mStatsMapB.insertEntry(new StatsMapKey(uid, 1, 0, 10), new StatsMapValue(0, 0, 0, 0));
+
+        mAppUidStatsMap.insertEntry(new UidStatsMapKey(uid), new StatsMapValue(10, 10000, 6, 6000));
+
+        mUidCounterSetMap.insertEntry(new U32(uid), new U8((short) 1));
+
+        assertTrue(cookieTagMapContainsUid(uid));
+        assertTrue(statsMapContainsUid(mStatsMapA, uid));
+        assertTrue(statsMapContainsUid(mStatsMapB, uid));
+        assertTrue(mAppUidStatsMap.containsKey(new UidStatsMapKey(uid)));
+        assertTrue(mUidCounterSetMap.containsKey(new U32(uid)));
+    }
+
+    @Test
+    public void testRemovingUidRemovesTagDataForUid() throws ErrnoException {
+        initBpfMapsWithTagData(UID_BLUE);
+        initBpfMapsWithTagData(UID_RED);
+
+        final Intent intent = new Intent(ACTION_UID_REMOVED);
+        intent.putExtra(EXTRA_UID, UID_BLUE);
+        mServiceContext.sendBroadcast(intent);
+
+        // assert that all UID_BLUE related tag data has been removed from the maps.
+        assertFalse(cookieTagMapContainsUid(UID_BLUE));
+        assertFalse(statsMapContainsUid(mStatsMapA, UID_BLUE));
+        assertFalse(statsMapContainsUid(mStatsMapB, UID_BLUE));
+        assertFalse(mAppUidStatsMap.containsKey(new UidStatsMapKey(UID_BLUE)));
+        assertFalse(mUidCounterSetMap.containsKey(new U32(UID_BLUE)));
+
+        // assert that UID_RED related tag data is still in the maps.
+        assertTrue(cookieTagMapContainsUid(UID_RED));
+        assertTrue(statsMapContainsUid(mStatsMapA, UID_RED));
+        assertTrue(statsMapContainsUid(mStatsMapB, UID_RED));
+        assertTrue(mAppUidStatsMap.containsKey(new UidStatsMapKey(UID_RED)));
+        assertTrue(mUidCounterSetMap.containsKey(new U32(UID_RED)));
+    }
 }