Merge "[Tethering] Add MTS method to TetheringManagerTest."
diff --git a/TEST_MAPPING b/TEST_MAPPING
index a5b97a1..4eeaf51 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
@@ -18,10 +22,40 @@
         }
       ]
     },
+    // Also run CtsNetTestCasesLatestSdk to ensure tests using older shims pass.
+    {
+      "name": "CtsNetTestCasesLatestSdk",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    // CTS tests that target older SDKs.
+    {
+      "name": "CtsNetTestCasesMaxTargetSdk31",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
     {
       "name": "bpf_existence_test"
     },
     {
+      "name": "connectivity_native_test"
+    },
+    {
+      "name": "libclat_test"
+    },
+    {
       "name": "netd_updatable_unit_test"
     },
     {
@@ -49,16 +83,10 @@
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
     },
     {
-      "name": "libclat_test"
-    },
-    {
       "name": "traffic_controller_unit_test",
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
     },
     {
-      "name": "libnetworkstats_test"
-    },
-    {
       "name": "FrameworksNetDeflakeTest"
     }
   ],
@@ -75,9 +103,23 @@
       ]
     },
     {
+      "name": "CtsNetTestCasesMaxTargetSdk31[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    {
       "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]"
     },
     {
@@ -127,9 +169,6 @@
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
-    },
-    {
-      "name": "TetheringCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "auto-postsubmit": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 41a0651..adcc236 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -21,7 +21,7 @@
 java_defaults {
     name: "TetheringApiLevel",
     sdk_version: "module_current",
-    target_sdk_version: "31",
+    target_sdk_version: "33",
     min_sdk_version: "30",
 }
 
@@ -33,6 +33,7 @@
         ":framework-connectivity-shared-srcs",
         ":tethering-module-utils-srcs",
         ":services-tethering-shared-srcs",
+        ":statslog-tethering-java-gen",
     ],
     static_libs: [
         "androidx.annotation_annotation",
@@ -47,6 +48,7 @@
         "net-utils-device-common-bpf",
         "net-utils-device-common-netlink",
         "netd-client",
+        "tetheringstatsprotos",
     ],
     libs: [
         "framework-connectivity",
@@ -211,3 +213,22 @@
     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",
+}
+
+genrule {
+    name: "statslog-tethering-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
+         " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+    out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
+}
\ No newline at end of file
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index dd04d6c..76c5d5c 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -69,9 +69,10 @@
     ],
     canned_fs_config: "canned_fs_config",
     bpfs: [
-        "clatd.o_mainline",
-        "netd.o_mainline",
+        "block.o",
+        "clatd.o",
         "dscp_policy.o",
+        "netd.o",
         "offload.o",
         "test.o",
     ],
@@ -89,6 +90,8 @@
     compressible: true,
 
     androidManifest: "AndroidManifest.xml",
+
+    compat_configs: ["connectivity-platform-compat-config"],
 }
 
 apex_key {
@@ -131,15 +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",
             "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/manifest.json b/Tethering/apex/manifest.json
index 88f13b2..3cb03ed 100644
--- a/Tethering/apex/manifest.json
+++ b/Tethering/apex/manifest.json
@@ -1,4 +1,4 @@
 {
   "name": "com.android.tethering",
-  "version": 319999900
+  "version": 339990000
 }
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/src/android/net/ITetheringEventCallback.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
index b4e3ba4..836761f 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
@@ -36,4 +36,5 @@
     void onTetherStatesChanged(in TetherStatesParcel states);
     void onTetherClientsChanged(in List<TetheredClient> clients);
     void onOffloadStatusChanged(int status);
+    void onSupportedTetheringTypes(long supportedBitmap);
 }
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
index 253eacb..f33f846 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
@@ -26,7 +26,7 @@
  * @hide
  */
 parcelable TetheringCallbackStartedParcel {
-    boolean tetheringSupported;
+    long supportedTypes;
     Network upstreamNetwork;
     TetheringConfigurationParcel config;
     TetherStatesParcel states;
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 6f9b33e..b3f0cf2 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -183,6 +183,12 @@
      */
     public static final int TETHERING_WIGIG = 6;
 
+    /**
+     * The int value of last tethering type.
+     * @hide
+     */
+    public static final int MAX_TETHERING_TYPE = TETHERING_WIGIG;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
@@ -520,6 +526,9 @@
         }
 
         @Override
+        public void onSupportedTetheringTypes(long supportedBitmap) { }
+
+        @Override
         public void onUpstreamChanged(Network network) { }
 
         @Override
@@ -1033,15 +1042,29 @@
         /**
          * Called when tethering supported status changed.
          *
+         * <p>This callback will be called immediately after the callback is
+         * registered, and never be called if there is changes afterward.
+         *
+         * <p>Tethering may be disabled via system properties, device configuration, or device
+         * policy restrictions.
+         *
+         * @param supported whether any tethering type is supported.
+         */
+        default void onTetheringSupported(boolean supported) {}
+
+        /**
+         * Called when tethering supported status changed.
+         *
          * <p>This will be called immediately after the callback is registered, and may be called
          * multiple times later upon changes.
          *
          * <p>Tethering may be disabled via system properties, device configuration, or device
          * policy restrictions.
          *
-         * @param supported The new supported status
+         * @param supportedTypes a set of @TetheringType which is supported.
+         * @hide
          */
-        default void onTetheringSupported(boolean supported) {}
+        default void onSupportedTetheringTypes(@NonNull Set<Integer> supportedTypes) {}
 
         /**
          * Called when tethering upstream changed.
@@ -1339,7 +1362,8 @@
                 @Override
                 public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
                     executor.execute(() -> {
-                        callback.onTetheringSupported(parcel.tetheringSupported);
+                        callback.onSupportedTetheringTypes(unpackBits(parcel.supportedTypes));
+                        callback.onTetheringSupported(parcel.supportedTypes != 0);
                         callback.onUpstreamChanged(parcel.upstreamNetwork);
                         sendErrorCallbacks(parcel.states);
                         sendRegexpsChanged(parcel.config);
@@ -1358,6 +1382,13 @@
                     });
                 }
 
+                @Override
+                public void onSupportedTetheringTypes(long supportedBitmap) {
+                    executor.execute(() -> {
+                        callback.onSupportedTetheringTypes(unpackBits(supportedBitmap));
+                    });
+                }
+
                 private void sendRegexpsChanged(TetheringConfigurationParcel parcel) {
                     callback.onTetherableInterfaceRegexpsChanged(new TetheringInterfaceRegexps(
                             parcel.tetherableBluetoothRegexs,
@@ -1396,6 +1427,23 @@
     }
 
     /**
+     * Unpack bitmap to a set of bit position intergers.
+     * @hide
+     */
+    public static ArraySet<Integer> unpackBits(long val) {
+        final ArraySet<Integer> result = new ArraySet<>(Long.bitCount(val));
+        int bitPos = 0;
+        while (val != 0) {
+            if ((val & 1) == 1) result.add(bitPos);
+
+            val = val >>> 1;
+            bitPos++;
+        }
+
+        return result;
+    }
+
+    /**
      * Remove tethering event callback previously registered with
      * {@link #registerTetheringEventCallback}.
      *
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index 5de4b97..904e491 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -4,6 +4,7 @@
 # module will be overwritten by the ones in the framework.
 rule com.android.internal.util.** com.android.networkstack.tethering.util.@1
 rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1
+rule android.util.IndentingPrintWriter* com.android.networkstack.tethering.util.AndroidUtilIndentingPrintWriter@1
 
 rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1
 
@@ -12,3 +13,8 @@
 
 # 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
+
+# Classes for hardware offload hidl interface
+rule android.hidl.base.V1_0.DebugInfo* com.android.networkstack.tethering.hidl.base.V1_0.DebugInfo@1
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 6735317..2905e28 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -12,6 +12,11 @@
     native <methods>;
 }
 
+# Ensure runtime-visible field annotations are kept when using R8 full mode.
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+-keep interface com.android.networkstack.tethering.util.Struct$Field {
+    *;
+}
 -keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
     *;
 }
@@ -19,3 +24,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/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index acd2625..437ed71 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -69,6 +69,7 @@
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 
@@ -241,6 +242,7 @@
     private final LinkProperties mLinkProperties;
     private final boolean mUsingLegacyDhcp;
     private final boolean mUsingBpfOffload;
+    private final int mP2pLeasesSubnetPrefixLength;
 
     private final Dependencies mDeps;
 
@@ -281,13 +283,15 @@
 
     private LinkAddress mIpv4Address;
 
+    private final TetheringMetrics mTetheringMetrics;
+
     // TODO: Add a dependency object to pass the data members or variables from the tethering
     // object. It helps to reduce the arguments of the constructor.
     public IpServer(
             String ifaceName, Looper looper, int interfaceType, SharedLog log,
             INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
             TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
-            Dependencies deps) {
+            TetheringMetrics tetheringMetrics, Dependencies deps) {
         super(ifaceName, looper);
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
@@ -299,8 +303,10 @@
         mLinkProperties = new LinkProperties();
         mUsingLegacyDhcp = config.useLegacyDhcpServer();
         mUsingBpfOffload = config.isBpfOffloadEnabled();
+        mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
         mPrivateAddressCoordinator = addressCoordinator;
         mDeps = deps;
+        mTetheringMetrics = tetheringMetrics;
         resetLinkProperties();
         mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
         mServingMode = STATE_AVAILABLE;
@@ -527,6 +533,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)
@@ -534,7 +543,8 @@
             .setServerAddr(serverAddr)
             .setMetered(true)
             .setSingleClientAddr(clientAddr)
-            .setChangePrefixOnDecline(changePrefixOnDecline);
+            .setChangePrefixOnDecline(changePrefixOnDecline)
+            .setLeasesSubnetPrefixLength(subnetPrefixLength);
             // TODO: also advertise link MTU
     }
 
@@ -1195,6 +1205,9 @@
             stopConntrackMonitoring();
 
             resetLinkProperties();
+
+            mTetheringMetrics.updateErrorCode(mInterfaceType, mLastError);
+            mTetheringMetrics.sendReport(mInterfaceType);
         }
 
         @Override
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 225bd58..c403548 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -65,15 +65,19 @@
 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;
 import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
 import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
 
+import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -118,6 +122,8 @@
     private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit");
     private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error");
     private static final String TETHER_DEV_MAP_PATH = makeMapPath("dev");
+    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+    private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
 
     // Using "," as a separator is safe because base64 characters are [0-9a-zA-Z/=+].
     private static final String DUMP_BASE64_DELIMITER = ",";
@@ -1019,7 +1025,7 @@
             map.forEach((k, v) -> {
                 pw.println(String.format("%s: %s", k, v));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping BPF stats map: " + e);
         }
     }
@@ -1067,12 +1073,13 @@
                 return;
             }
             map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v)));
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping IPv6 upstream map: " + e);
         }
     }
 
-    private String ipv4RuleToBase64String(Tether4Key key, Tether4Value value) {
+    private <K extends Struct, V extends Struct> String bpfMapEntryToBase64String(
+            final K key, final V value) {
         final byte[] keyBytes = key.writeToBytes();
         final String keyBase64Str = Base64.encodeToString(keyBytes, Base64.DEFAULT)
                 .replace("\n", "");
@@ -1083,28 +1090,45 @@
         return keyBase64Str + DUMP_BASE64_DELIMITER + valueBase64Str;
     }
 
-    private void dumpRawIpv4ForwardingRuleMap(
-            BpfMap<Tether4Key, Tether4Value> map, IndentingPrintWriter pw) throws ErrnoException {
+    private <K extends Struct, V extends Struct> void dumpRawMap(BpfMap<K, V> map,
+            IndentingPrintWriter pw) throws ErrnoException {
         if (map == null) {
-            pw.println("No IPv4 support");
+            pw.println("No BPF support");
             return;
         }
         if (map.isEmpty()) {
-            pw.println("No rules");
+            pw.println("No entries");
             return;
         }
-        map.forEach((k, v) -> pw.println(ipv4RuleToBase64String(k, v)));
+        map.forEach((k, v) -> pw.println(bpfMapEntryToBase64String(k, v)));
     }
 
     /**
      * Dump raw BPF map in base64 encoded strings. For test only.
+     * Only allow to dump one map path once.
+     * Format:
+     * $ dumpsys tethering bpfRawMap --<map name>
      */
-    public void dumpRawMap(@NonNull IndentingPrintWriter pw) {
-        try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
-            // TODO: dump downstream map.
-            dumpRawIpv4ForwardingRuleMap(upstreamMap, pw);
-        } catch (ErrnoException e) {
-            pw.println("Error dumping IPv4 map: " + e);
+    public void dumpRawMap(@NonNull IndentingPrintWriter pw, @Nullable String[] args) {
+        // TODO: consider checking the arg order that <map name> is after "bpfRawMap". Probably
+        // it is okay for now because this is used by test only and test is supposed to use
+        // expected argument order.
+        // TODO: dump downstream4 map.
+        if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
+            try (BpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
+                dumpRawMap(statsMap, pw);
+            } catch (ErrnoException | IOException e) {
+                pw.println("Error dumping stats map: " + e);
+            }
+            return;
+        }
+        if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
+            try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
+                dumpRawMap(upstreamMap, pw);
+            } catch (ErrnoException | IOException e) {
+                pw.println("Error dumping IPv4 map: " + e);
+            }
+            return;
         }
     }
 
@@ -1172,7 +1196,7 @@
             pw.increaseIndent();
             dumpIpv4ForwardingRuleMap(now, DOWNSTREAM, downstreamMap, pw);
             pw.decreaseIndent();
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping IPv4 map: " + e);
         }
     }
@@ -1197,7 +1221,7 @@
                 }
                 if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping counter map: " + e);
         }
     }
@@ -1221,7 +1245,7 @@
                 pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
                         v.ifIndex, getIfName(v.ifIndex)));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping dev map: " + e);
         }
         pw.decreaseIndent();
@@ -2033,5 +2057,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/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index 844efde..adc95ab 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -34,7 +34,6 @@
 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;
@@ -48,12 +47,10 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Parcel;
-import android.os.PersistableBundle;
 import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.provider.Settings;
-import android.telephony.CarrierConfigManager;
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -307,13 +304,13 @@
         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)) {
+
+        if (!config.isCarrierSupportTethering) {
             // To block tethering, behave as if running provisioning check and failed.
             return TETHERING_PROVISIONING_CARRIER_UNSUPPORT;
         }
 
-        if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+        if (!config.isCarrierConfigAffirmsEntitlementCheckRequired) {
             return TETHERING_PROVISIONING_NOT_REQUIRED;
         }
         return (config.provisioningApp.length == 2)
@@ -380,57 +377,6 @@
     }
 
     /**
-     * Get carrier configuration bundle.
-     * @param config an object that encapsulates the various tethering configuration elements.
-     * */
-    public PersistableBundle getCarrierConfig(final TetheringConfiguration config) {
-        final CarrierConfigManager configManager = mContext
-                .getSystemService(CarrierConfigManager.class);
-        if (configManager == null) return null;
-
-        final PersistableBundle carrierConfig = configManager.getConfigForSubId(
-                config.activeDataSubId);
-
-        if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
-            return carrierConfig;
-        }
-
-        return null;
-    }
-
-    // The logic here is aimed solely at confirming that a CarrierConfig exists
-    // and affirms that entitlement checks are not required.
-    //
-    // 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
-        final PersistableBundle carrierConfig = getCarrierConfig(config);
-        if (carrierConfig == null) return false;
-
-        // A CarrierConfigManager was found and it has a config.
-        final boolean isEntitlementCheckRequired = carrierConfig.getBoolean(
-                CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
-        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.
@@ -479,7 +425,7 @@
 
     private void runTetheringProvisioning(
             boolean showProvisioningUi, int downstreamType, final TetheringConfiguration config) {
-        if (carrierConfigAffirmsCarrierNotSupport(config)) {
+        if (!config.isCarrierSupportTethering) {
             mListener.onTetherProvisioningFailed(downstreamType, "Carrier does not support.");
             if (showProvisioningUi) {
                 showCarrierUnsupportedDialog();
@@ -497,7 +443,7 @@
     }
 
     private void showCarrierUnsupportedDialog() {
-        // This is only used when carrierConfigAffirmsCarrierNotSupport() is true.
+        // This is only used when TetheringConfiguration.isCarrierSupportTethering is false.
         if (!SdkLevel.isAtLeastT()) {
             return;
         }
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index cc2422f..41a10ae 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -172,6 +172,9 @@
             return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
         }
 
+        // This ensures that tethering isn't started on 2 different interfaces with the same type.
+        // Once tethering could support multiple interface with the same type,
+        // TetheringSoftApCallback would need to handle it among others.
         final LinkAddress cachedAddress = mCachedAddresses.get(ipServer.interfaceType());
         if (useLastAddress && cachedAddress != null
                 && !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
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 0b607bd..af017f3 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -135,10 +135,12 @@
 import com.android.internal.util.StateMachine;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.CollectionUtils;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.networkstack.tethering.util.TetheringUtils;
@@ -253,6 +255,7 @@
     private final UserManager mUserManager;
     private final BpfCoordinator mBpfCoordinator;
     private final PrivateAddressCoordinator mPrivateAddressCoordinator;
+    private final TetheringMetrics mTetheringMetrics;
     private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
 
     private volatile TetheringConfiguration mConfig;
@@ -278,6 +281,11 @@
     private BluetoothPan mBluetoothPan;
     private PanServiceListener mBluetoothPanListener;
     private ArrayList<Pair<Boolean, IIntResultListener>> mPendingPanRequests;
+    // AIDL doesn't support Set<Integer>. Maintain a int bitmap here. When the bitmap is passed to
+    // TetheringManager, TetheringManager would convert it to a set of Integer types.
+    // mSupportedTypeBitmap should always be updated inside tethering internal thread but it may be
+    // read from binder thread which called TetheringService directly.
+    private volatile long mSupportedTypeBitmap;
 
     public Tethering(TetheringDependencies deps) {
         mLog.mark("Tethering.constructed");
@@ -286,6 +294,7 @@
         mNetd = mDeps.getINetd(mContext);
         mLooper = mDeps.getTetheringLooper();
         mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
+        mTetheringMetrics = mDeps.getTetheringMetrics();
 
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -439,8 +448,22 @@
                 mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
 
         final WifiManager wifiManager = getWifiManager();
+        TetheringSoftApCallback softApCallback = new TetheringSoftApCallback();
         if (wifiManager != null) {
-            wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
+            wifiManager.registerSoftApCallback(mExecutor, softApCallback);
+        }
+        if (SdkLevel.isAtLeastT() && wifiManager != null) {
+            try {
+                // Although WifiManager#registerLocalOnlyHotspotSoftApCallback document that it need
+                // NEARBY_WIFI_DEVICES permission, but actually a caller who have NETWORK_STACK
+                // or MAINLINE_NETWORK_STACK permission would also able to use this API.
+                wifiManager.registerLocalOnlyHotspotSoftApCallback(mExecutor, softApCallback);
+            } catch (UnsupportedOperationException e) {
+                // Since wifi module development in internal branch,
+                // #registerLocalOnlyHotspotSoftApCallback currently doesn't supported in AOSP
+                // before AOSP switch to Android T + 1.
+                Log.wtf(TAG, "registerLocalOnlyHotspotSoftApCallback API is not supported");
+            }
         }
 
         startTrackDefaultNetwork();
@@ -476,7 +499,7 @@
             // To avoid launching unexpected provisioning checks, ignore re-provisioning
             // when no CarrierConfig loaded yet. Assume reevaluateSimCardProvisioning()
             // will be triggered again when CarrierConfig is loaded.
-            if (mEntitlementMgr.getCarrierConfig(mConfig) != null) {
+            if (TetheringConfiguration.getCarrierConfig(mContext, subId) != null) {
                 mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
             } else {
                 mLog.log("IGNORED reevaluate provisioning, no carrier config loaded");
@@ -494,6 +517,8 @@
         mUpstreamNetworkMonitor.setUpstreamConfig(mConfig.chooseUpstreamAutomatically,
                 mConfig.isDunRequired);
         reportConfigurationChanged(mConfig.toStableParcelable());
+
+        updateSupportedDownstreams(mConfig);
     }
 
     private void maybeDunSettingChanged() {
@@ -534,6 +559,13 @@
         }
 
         // Called by wifi when the number of soft AP clients changed.
+        // Currently multiple softAp would not behave well in PrivateAddressCoordinator
+        // (where it gets the address from cache), it ensure tethering only support one ipServer for
+        // TETHERING_WIFI. Once tethering support multiple softAp enabled simultaneously,
+        // onConnectedClientsChanged should also be updated to support tracking different softAp's
+        // clients individually.
+        // TODO: Add wtf log and have check to reject request duplicated type with different
+        // interface.
         @Override
         public void onConnectedClientsChanged(final List<WifiClient> clients) {
             updateConnectedClients(clients);
@@ -608,7 +640,8 @@
         processInterfaceStateChange(iface, false /* enabled */);
     }
 
-    void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) {
+    void startTethering(final TetheringRequestParcel request, final String callerPkg,
+            final IIntResultListener listener) {
         mHandler.post(() -> {
             final TetheringRequestParcel unfinishedRequest = mActiveTetheringRequests.get(
                     request.tetheringType);
@@ -628,6 +661,7 @@
                         request.showProvisioningUi);
             }
             enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
+            mTetheringMetrics.createBuilder(request.tetheringType, callerPkg);
         });
     }
 
@@ -687,7 +721,11 @@
 
         // If changing tethering fail, remove corresponding request
         // no matter who trigger the start/stop.
-        if (result != TETHER_ERROR_NO_ERROR) mActiveTetheringRequests.remove(type);
+        if (result != TETHER_ERROR_NO_ERROR) {
+            mActiveTetheringRequests.remove(type);
+            mTetheringMetrics.updateErrorCode(type, result);
+            mTetheringMetrics.sendReport(type);
+        }
     }
 
     private int setWifiTethering(final boolean enable) {
@@ -1280,7 +1318,7 @@
 
             // Finally bring up serving on the new interface
             mWifiP2pTetherInterface = group.getInterface();
-            enableWifiIpServing(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
+            enableWifiP2pIpServing(mWifiP2pTetherInterface);
         }
 
         private void handleUserRestrictionAction() {
@@ -1371,20 +1409,22 @@
         changeInterfaceState(ifname, ipServingMode);
     }
 
-    private void disableWifiIpServingCommon(int tetheringType, String ifname, int apState) {
-        mLog.log("Canceling WiFi tethering request -"
-                + " type=" + tetheringType
-                + " interface=" + ifname
-                + " state=" + apState);
-
-        if (!TextUtils.isEmpty(ifname)) {
-            final TetherState ts = mTetherStates.get(ifname);
-            if (ts != null) {
-                ts.ipServer.unwanted();
-                return;
-            }
+    private void disableWifiIpServingCommon(int tetheringType, String ifname) {
+        if (!TextUtils.isEmpty(ifname) && mTetherStates.containsKey(ifname)) {
+            mTetherStates.get(ifname).ipServer.unwanted();
+            return;
         }
 
+        if (SdkLevel.isAtLeastT()) {
+            mLog.e("Tethering no longer handle untracked interface after T: " + ifname);
+            return;
+        }
+
+        // Attempt to guess the interface name before T. Pure AOSP code should never enter here
+        // because WIFI_AP_STATE_CHANGED intent always include ifname and it should be tracked
+        // by mTetherStates. In case OEMs have some modification in wifi side which pass null
+        // or empty ifname. Before T, tethering allow to disable the first wifi ipServer if
+        // given ifname don't match any tracking ipServer.
         for (int i = 0; i < mTetherStates.size(); i++) {
             final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
             if (ipServer.interfaceType() == tetheringType) {
@@ -1392,7 +1432,6 @@
                 return;
             }
         }
-
         mLog.log("Error disabling Wi-Fi IP serving; "
                 + (TextUtils.isEmpty(ifname) ? "no interface name specified"
                                            : "specified interface: " + ifname));
@@ -1401,20 +1440,39 @@
     private void disableWifiIpServing(String ifname, int apState) {
         // Regardless of whether we requested this transition, the AP has gone
         // down.  Don't try to tether again unless we're requested to do so.
-        // TODO: Remove this altogether, once Wi-Fi reliably gives us an
-        // interface name with every broadcast.
         mWifiTetherRequested = false;
 
-        disableWifiIpServingCommon(TETHERING_WIFI, ifname, apState);
+        mLog.log("Canceling WiFi tethering request - interface=" + ifname + " state=" + apState);
+
+        disableWifiIpServingCommon(TETHERING_WIFI, ifname);
+    }
+
+    private void enableWifiP2pIpServing(String ifname) {
+        if (TextUtils.isEmpty(ifname)) {
+            mLog.e("Cannot enable P2P IP serving with invalid interface");
+            return;
+        }
+
+        // After T, tethering always trust the iface pass by state change intent. This allow
+        // tethering to deprecate tetherable p2p regexs after T.
+        final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI_P2P : ifaceNameToType(ifname);
+        if (!checkTetherableType(type)) {
+            mLog.e(ifname + " is not a tetherable iface, ignoring");
+            return;
+        }
+        enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
     }
 
     private void disableWifiP2pIpServingIfNeeded(String ifname) {
         if (TextUtils.isEmpty(ifname)) return;
 
-        disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
+        mLog.log("Canceling P2P tethering request - interface=" + ifname);
+        disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
     }
 
     private void enableWifiIpServing(String ifname, int wifiIpMode) {
+        mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
+
         // Map wifiIpMode values to IpServer.Callback serving states, inferring
         // from mWifiTetherRequested as a final "best guess".
         final int ipServingMode;
@@ -1430,13 +1488,18 @@
                 return;
         }
 
+        // After T, tethering always trust the iface pass by state change intent. This allow
+        // tethering to deprecate tetherable wifi regexs after T.
+        final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+        if (!checkTetherableType(type)) {
+            mLog.e(ifname + " is not a tetherable iface, ignoring");
+            return;
+        }
+
         if (!TextUtils.isEmpty(ifname)) {
-            ensureIpServerStarted(ifname);
-            changeInterfaceState(ifname, ipServingMode);
+            enableIpServing(type, ifname, ipServingMode);
         } else {
-            mLog.e(String.format(
-                    "Cannot enable IP serving in mode %s on missing interface name",
-                    ipServingMode));
+            mLog.e("Cannot enable IP serving on missing interface name");
         }
     }
 
@@ -1513,26 +1576,6 @@
         return mConfig;
     }
 
-    boolean hasAnySupportedDownstream() {
-        if ((mConfig.tetherableUsbRegexs.length != 0)
-                || (mConfig.tetherableWifiRegexs.length != 0)
-                || (mConfig.tetherableBluetoothRegexs.length != 0)) {
-            return true;
-        }
-
-        // 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;
     }
@@ -2322,7 +2365,7 @@
         mHandler.post(() -> {
             mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
             final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
-            parcel.tetheringSupported = isTetheringSupported();
+            parcel.supportedTypes = mSupportedTypeBitmap;
             parcel.upstreamNetwork = mTetherUpstream;
             parcel.config = mConfig.toStableParcelable();
             parcel.states =
@@ -2361,6 +2404,22 @@
         });
     }
 
+    private void reportTetheringSupportedChange(final long supportedBitmap) {
+        final int length = mTetheringEventCallbacks.beginBroadcast();
+        try {
+            for (int i = 0; i < length; i++) {
+                try {
+                    mTetheringEventCallbacks.getBroadcastItem(i).onSupportedTetheringTypes(
+                            supportedBitmap);
+                } catch (RemoteException e) {
+                    // Not really very much to do here.
+                }
+            }
+        } finally {
+            mTetheringEventCallbacks.finishBroadcast();
+        }
+    }
+
     private void reportUpstreamChanged(UpstreamNetworkState ns) {
         final int length = mTetheringEventCallbacks.beginBroadcast();
         final Network network = (ns != null) ? ns.network : null;
@@ -2445,18 +2504,56 @@
         }
     }
 
+    private void updateSupportedDownstreams(final TetheringConfiguration config) {
+        final long preSupportedBitmap = mSupportedTypeBitmap;
+
+        if (!isTetheringAllowed() || mEntitlementMgr.isProvisioningNeededButUnavailable()) {
+            mSupportedTypeBitmap = 0;
+        } else {
+            mSupportedTypeBitmap = makeSupportedDownstreams(config);
+        }
+
+        if (preSupportedBitmap != mSupportedTypeBitmap) {
+            reportTetheringSupportedChange(mSupportedTypeBitmap);
+        }
+    }
+
+    private long makeSupportedDownstreams(final TetheringConfiguration config) {
+        long types = 0;
+        if (config.tetherableUsbRegexs.length != 0) types |= (1 << TETHERING_USB);
+
+        if (config.tetherableWifiRegexs.length != 0) types |= (1 << TETHERING_WIFI);
+
+        if (config.tetherableBluetoothRegexs.length != 0) types |= (1 << TETHERING_BLUETOOTH);
+
+        // 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() && types == 0) return types;
+
+        if (config.tetherableNcmRegexs.length != 0) types |= (1 << TETHERING_NCM);
+
+        if (config.tetherableWifiP2pRegexs.length != 0) types |= (1 << TETHERING_WIFI_P2P);
+
+        if (isEthernetSupported()) types |= (1 << TETHERING_ETHERNET);
+
+        return types;
+    }
+
     // if ro.tether.denied = true we default to no tethering
     // gservices could set the secure setting to 1 though to enable it on a build where it
     // had previously been turned off.
-    boolean isTetheringSupported() {
+    private boolean isTetheringAllowed() {
         final int defaultVal = mDeps.isTetheringDenied() ? 0 : 1;
         final boolean tetherSupported = Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.TETHER_SUPPORTED, defaultVal) != 0;
-        final boolean tetherEnabledInSettings = tetherSupported
+        return tetherSupported
                 && !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING);
+    }
 
-        return tetherEnabledInSettings && hasAnySupportedDownstream()
-                && !mEntitlementMgr.isProvisioningNeededButUnavailable();
+    boolean isTetheringSupported() {
+        return mSupportedTypeBitmap > 0;
     }
 
     private void dumpBpf(IndentingPrintWriter pw) {
@@ -2472,13 +2569,12 @@
                 writer, "  ");
 
         // Used for testing instead of human debug.
-        // TODO: add options to choose which map to dump.
-        if (argsContain(args, "bpfRawMap")) {
-            mBpfCoordinator.dumpRawMap(pw);
+        if (CollectionUtils.contains(args, "bpfRawMap")) {
+            mBpfCoordinator.dumpRawMap(pw, args);
             return;
         }
 
-        if (argsContain(args, "bpf")) {
+        if (CollectionUtils.contains(args, "bpf")) {
             dumpBpf(pw);
             return;
         }
@@ -2544,7 +2640,7 @@
 
         pw.println("Log:");
         pw.increaseIndent();
-        if (argsContain(args, "--short")) {
+        if (CollectionUtils.contains(args, "--short")) {
             pw.println("<log removed for brevity>");
         } else {
             mLog.dump(fd, pw, args);
@@ -2588,13 +2684,6 @@
         if (e != null) throw e;
     }
 
-    private static boolean argsContain(String[] args, String target) {
-        for (String arg : args) {
-            if (target.equals(arg)) return true;
-        }
-        return false;
-    }
-
     private void updateConnectedClients(final List<WifiClient> wifiClients) {
         if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(),
                 wifiClients)) {
@@ -2681,23 +2770,28 @@
         mTetherMainSM.sendMessage(which, state, 0, newLp);
     }
 
+    private boolean hasSystemFeature(final String feature) {
+        return mContext.getPackageManager().hasSystemFeature(feature);
+    }
+
+    private boolean checkTetherableType(int type) {
+        if ((type == TETHERING_WIFI || type == TETHERING_WIGIG)
+                && !hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            return false;
+        }
+
+        if (type == TETHERING_WIFI_P2P && !hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
+            return false;
+        }
+
+        return type != TETHERING_INVALID;
+    }
+
     private void ensureIpServerStarted(final String iface) {
         // If we don't care about this type of interface, ignore.
         final int interfaceType = ifaceNameToType(iface);
-        if (interfaceType == TETHERING_INVALID) {
-            mLog.log(iface + " is not a tetherable iface, ignoring");
-            return;
-        }
-
-        final PackageManager pm = mContext.getPackageManager();
-        if ((interfaceType == TETHERING_WIFI || interfaceType == TETHERING_WIGIG)
-                && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
-            mLog.log(iface + " is not tetherable, because WiFi feature is disabled");
-            return;
-        }
-        if (interfaceType == TETHERING_WIFI_P2P
-                && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
-            mLog.log(iface + " is not tetherable, because WiFi Direct feature is disabled");
+        if (!checkTetherableType(interfaceType)) {
+            mLog.log(iface + " is used for " + interfaceType + " which is not tetherable");
             return;
         }
 
@@ -2715,7 +2809,7 @@
         final TetherState tetherState = new TetherState(
                 new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
                              makeControlCallback(), mConfig, mPrivateAddressCoordinator,
-                             mDeps.getIpServerDependencies()), isNcm);
+                             mTetheringMetrics, 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 eaf8589..7c36054 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -24,14 +24,17 @@
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.TetheringConfigurationParcel;
 import android.net.util.SharedLog;
+import android.os.PersistableBundle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -142,6 +145,9 @@
     public final int provisioningCheckPeriod;
     public final String provisioningResponse;
 
+    public final boolean isCarrierSupportTethering;
+    public final boolean isCarrierConfigAffirmsEntitlementCheckRequired;
+
     public final int activeDataSubId;
 
     private final boolean mEnableLegacyDhcpServer;
@@ -149,6 +155,7 @@
     // TODO: Add to TetheringConfigurationParcel if required.
     private final boolean mEnableBpfOffload;
     private final boolean mEnableWifiP2pDedicatedIp;
+    private final int mP2pLeasesSubnetPrefixLength;
 
     private final int mUsbTetheringFunction;
     protected final ContentResolver mContentResolver;
@@ -206,6 +213,11 @@
         provisioningResponse = getResourceString(res,
                 R.string.config_mobile_hotspot_provision_response);
 
+        PersistableBundle carrierConfigs = getCarrierConfig(ctx, activeDataSubId);
+        isCarrierSupportTethering = carrierConfigAffirmsCarrierSupport(carrierConfigs);
+        isCarrierConfigAffirmsEntitlementCheckRequired =
+                carrierConfigAffirmsEntitlementCheckRequired(carrierConfigs);
+
         mOffloadPollInterval = getResourceInteger(res,
                 R.integer.config_tether_offload_poll_interval,
                 DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
@@ -214,9 +226,27 @@
                 R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip,
                 false /* 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;
@@ -272,6 +302,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: ");
@@ -301,6 +340,10 @@
         pw.print("provisioningAppNoUi: ");
         pw.println(provisioningAppNoUi);
 
+        pw.println("isCarrierSupportTethering: " + isCarrierSupportTethering);
+        pw.println("isCarrierConfigAffirmsEntitlementCheckRequired: "
+                + isCarrierConfigAffirmsEntitlementCheckRequired);
+
         pw.print("enableBpfOffload: ");
         pw.println(mEnableBpfOffload);
 
@@ -310,6 +353,9 @@
         pw.print("enableWifiP2pDedicatedIp: ");
         pw.println(mEnableWifiP2pDedicatedIp);
 
+        pw.print("p2pLeasesSubnetPrefixLength: ");
+        pw.println(mP2pLeasesSubnetPrefixLength);
+
         pw.print("mUsbTetheringFunction: ");
         pw.println(isUsingNcm() ? "NCM" : "RNDIS");
     }
@@ -330,6 +376,9 @@
                 toIntArray(preferredUpstreamIfaceTypes)));
         sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
         sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi));
+        sj.add(String.format("isCarrierSupportTethering:%s", isCarrierSupportTethering));
+        sj.add(String.format("isCarrierConfigAffirmsEntitlementCheckRequired:%s",
+                isCarrierConfigAffirmsEntitlementCheckRequired));
         sj.add(String.format("enableBpfOffload:%s", mEnableBpfOffload));
         sj.add(String.format("enableLegacyDhcpServer:%s", mEnableLegacyDhcpServer));
         return String.format("TetheringConfiguration{%s}", sj.toString());
@@ -565,6 +614,39 @@
         return result;
     }
 
+    private static boolean carrierConfigAffirmsEntitlementCheckRequired(
+            PersistableBundle carrierConfig) {
+        if (carrierConfig == null) {
+            return true;
+        }
+        return carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
+    }
+
+    private static boolean carrierConfigAffirmsCarrierSupport(PersistableBundle carrierConfig) {
+        if (!SdkLevel.isAtLeastT() || carrierConfig == null) {
+            return true;
+        }
+        return carrierConfig.getBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, true);
+    }
+
+    /**
+     * Get carrier configuration bundle.
+     */
+    public static PersistableBundle getCarrierConfig(Context context, int activeDataSubId) {
+        final CarrierConfigManager configManager =
+                context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return null;
+        }
+
+        final PersistableBundle carrierConfig = configManager.getConfigForSubId(activeDataSubId);
+        if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
+            return carrierConfig;
+        }
+        return null;
+    }
+
     /**
      * Convert this TetheringConfiguration to a TetheringConfigurationParcel.
      */
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 9224213..8e0354d 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -34,6 +34,7 @@
 import com.android.internal.util.StateMachine;
 import com.android.networkstack.apishim.BluetoothPanShimImpl;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 
 import java.util.ArrayList;
 
@@ -163,4 +164,11 @@
     public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
         return BluetoothPanShimImpl.newInstance(pan);
     }
+
+    /**
+     * Get a reference to the TetheringMetrics to be used by tethering.
+     */
+    public TetheringMetrics getTetheringMetrics() {
+        return new TetheringMetrics();
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 9fb61fe..f147e10 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -137,7 +137,7 @@
                 return;
             }
 
-            mTethering.startTethering(request, listener);
+            mTethering.startTethering(request, callerPkg, listener);
         }
 
         @Override
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
new file mode 100644
index 0000000..e25f2ae
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -0,0 +1,195 @@
+/*
+ * 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.metrics;
+
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+
+import android.stats.connectivity.DownstreamType;
+import android.stats.connectivity.ErrorCode;
+import android.stats.connectivity.UpstreamType;
+import android.stats.connectivity.UserType;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Collection of utilities for tethering metrics.
+ *
+ * To see if the logs are properly sent to statsd, execute following commands
+ *
+ * $ adb shell cmd stats print-logs
+ * $ adb logcat | grep statsd OR $ adb logcat -b stats
+ *
+ * @hide
+ */
+public class TetheringMetrics {
+    private static final String TAG = TetheringMetrics.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final String SETTINGS_PKG_NAME = "com.android.settings";
+    private static final String SYSTEMUI_PKG_NAME = "com.android.systemui";
+    private static final String GMS_PKG_NAME = "com.google.android.gms";
+    private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
+
+    /** Update Tethering stats about caller's package name and downstream type. */
+    public void createBuilder(final int downstreamType, final String callerPkg) {
+        mBuilderMap.clear();
+        NetworkTetheringReported.Builder statsBuilder =
+                    NetworkTetheringReported.newBuilder();
+        statsBuilder.setDownstreamType(downstreamTypeToEnum(downstreamType))
+                    .setUserType(userTypeToEnum(callerPkg))
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(ErrorCode.EC_NO_ERROR)
+                    .build();
+        mBuilderMap.put(downstreamType, statsBuilder);
+    }
+
+    /** Update error code of given downstreamType. */
+    public void updateErrorCode(final int downstreamType, final int errCode) {
+        NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
+        if (statsBuilder == null) {
+            Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
+            return;
+        }
+        statsBuilder.setErrorCode(errorCodeToEnum(errCode));
+    }
+
+    /** Remove Tethering stats.
+     *  If Tethering stats is ready to write then write it before removing.
+     */
+    public void sendReport(final int downstreamType) {
+        final NetworkTetheringReported.Builder statsBuilder =
+                mBuilderMap.get(downstreamType);
+        if (statsBuilder == null) {
+            Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
+            return;
+        }
+        write(statsBuilder.build());
+        mBuilderMap.remove(downstreamType);
+    }
+
+    /** Collect Tethering stats and write metrics data to statsd pipeline. */
+    @VisibleForTesting
+    public void write(@NonNull final NetworkTetheringReported reported) {
+        TetheringStatsLog.write(TetheringStatsLog.NETWORK_TETHERING_REPORTED,
+                reported.getErrorCode().getNumber(),
+                reported.getDownstreamType().getNumber(),
+                reported.getUpstreamType().getNumber(),
+                reported.getUserType().getNumber());
+        if (DBG) {
+            Log.d(TAG, "Write errorCode: " + reported.getErrorCode().getNumber()
+                    + ", downstreamType: " + reported.getDownstreamType().getNumber()
+                    + ", upstreamType: " + reported.getUpstreamType().getNumber()
+                    + ", userType: " + reported.getUserType().getNumber());
+        }
+    }
+
+    /** Map {@link TetheringType} to {@link DownstreamType} */
+    private DownstreamType downstreamTypeToEnum(final int ifaceType) {
+        switch(ifaceType) {
+            case TETHERING_WIFI:
+                return DownstreamType.DS_TETHERING_WIFI;
+            case TETHERING_WIFI_P2P:
+                return DownstreamType.DS_TETHERING_WIFI_P2P;
+            case TETHERING_USB:
+                return DownstreamType.DS_TETHERING_USB;
+            case TETHERING_BLUETOOTH:
+                return DownstreamType.DS_TETHERING_BLUETOOTH;
+            case TETHERING_NCM:
+                return DownstreamType.DS_TETHERING_NCM;
+            case TETHERING_ETHERNET:
+                return DownstreamType.DS_TETHERING_ETHERNET;
+            default:
+                return DownstreamType.DS_UNSPECIFIED;
+        }
+    }
+
+    /** Map {@link StartTetheringError} to {@link ErrorCode} */
+    private ErrorCode errorCodeToEnum(final int lastError) {
+        switch(lastError) {
+            case TETHER_ERROR_NO_ERROR:
+                return ErrorCode.EC_NO_ERROR;
+            case TETHER_ERROR_UNKNOWN_IFACE:
+                return ErrorCode.EC_UNKNOWN_IFACE;
+            case TETHER_ERROR_SERVICE_UNAVAIL:
+                return ErrorCode.EC_SERVICE_UNAVAIL;
+            case TETHER_ERROR_UNSUPPORTED:
+                return ErrorCode.EC_UNSUPPORTED;
+            case TETHER_ERROR_UNAVAIL_IFACE:
+                return ErrorCode.EC_UNAVAIL_IFACE;
+            case TETHER_ERROR_INTERNAL_ERROR:
+                return ErrorCode.EC_INTERNAL_ERROR;
+            case TETHER_ERROR_TETHER_IFACE_ERROR:
+                return ErrorCode.EC_TETHER_IFACE_ERROR;
+            case TETHER_ERROR_UNTETHER_IFACE_ERROR:
+                return ErrorCode.EC_UNTETHER_IFACE_ERROR;
+            case TETHER_ERROR_ENABLE_FORWARDING_ERROR:
+                return ErrorCode.EC_ENABLE_FORWARDING_ERROR;
+            case TETHER_ERROR_DISABLE_FORWARDING_ERROR:
+                return ErrorCode.EC_DISABLE_FORWARDING_ERROR;
+            case TETHER_ERROR_IFACE_CFG_ERROR:
+                return ErrorCode.EC_IFACE_CFG_ERROR;
+            case TETHER_ERROR_PROVISIONING_FAILED:
+                return ErrorCode.EC_PROVISIONING_FAILED;
+            case TETHER_ERROR_DHCPSERVER_ERROR:
+                return ErrorCode.EC_DHCPSERVER_ERROR;
+            case TETHER_ERROR_ENTITLEMENT_UNKNOWN:
+                return ErrorCode.EC_ENTITLEMENT_UNKNOWN;
+            case TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:
+                return ErrorCode.EC_NO_CHANGE_TETHERING_PERMISSION;
+            case TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION:
+                return ErrorCode.EC_NO_ACCESS_TETHERING_PERMISSION;
+            default:
+                return ErrorCode.EC_UNKNOWN_TYPE;
+        }
+    }
+
+    /** Map callerPkg to {@link UserType} */
+    private UserType userTypeToEnum(final String callerPkg) {
+        if (callerPkg.equals(SETTINGS_PKG_NAME)) {
+            return UserType.USER_SETTINGS;
+        } else if (callerPkg.equals(SYSTEMUI_PKG_NAME)) {
+            return UserType.USER_SYSTEMUI;
+        } else if (callerPkg.equals(GMS_PKG_NAME)) {
+            return UserType.USER_GMS;
+        } else {
+            return UserType.USER_UNKNOWN;
+        }
+    }
+}
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 6eaf68b..31c3df3 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -49,7 +49,7 @@
 // Use with NetworkStackJarJarRules.
 android_library {
     name: "TetheringIntegrationTestsLatestSdkLib",
-    target_sdk_version: "31",
+    target_sdk_version: "33",
     platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     visibility: [
@@ -83,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 de81a38..92be84d 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -27,18 +27,24 @@
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringTester.RemoteResponder;
+import static android.net.TetheringTester.isIcmpv6Type;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
 import static com.android.net.module.util.HexDump.dumpHexString;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.testutils.DeviceInfoUtils.KVersion;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 
 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;
@@ -59,6 +65,7 @@
 import android.os.HandlerThread;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
 import android.text.TextUtils;
 import android.util.Base64;
 import android.util.Log;
@@ -70,10 +77,13 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.Ipv6Utils;
 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.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.Ipv4Header;
@@ -82,6 +92,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TapPacketReader;
@@ -95,6 +106,7 @@
 
 import java.io.FileDescriptor;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InterfaceAddress;
 import java.net.NetworkInterface;
@@ -128,17 +140,33 @@
     // 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;
+    // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+    private static final int RX_UDP_PACKET_SIZE = 30;
+    private static final int RX_UDP_PACKET_COUNT = 456;
+    // Per TX UDP packet size: ethhdr (14) + iphdr (20) + udphdr (8) + payload (2) = 44 bytes.
+    private static final int TX_UDP_PACKET_SIZE = 44;
+    private static final int TX_UDP_PACKET_COUNT = 123;
+
     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");
     private static final InetAddress TEST_IP6_DNS = parseNumericAddress("2001:db8:1::888");
+    private static final IpPrefix TEST_NAT64PREFIX = new IpPrefix("64:ff9b::/96");
+    private static final Inet6Address REMOTE_NAT64_ADDR =
+            (Inet6Address) parseNumericAddress("64:ff9b::808:808");
     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 DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+    private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
     private static final String BASE64_DELIMITER = ",";
     private static final String LINE_DELIMITER = "\\n";
 
+    // version=6, traffic class=0x0, flowlabel=0x0;
+    private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x60000000;
+    private static final short HOP_LIMIT = 0x40;
+
     private final Context mContext = InstrumentationRegistry.getContext();
     private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
     private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
@@ -168,12 +196,13 @@
         mUiAutomation.adoptShellPermissionIdentity(
                 MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED, ACCESS_NETWORK_STATE,
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS, DUMP);
-        mRunTests = mTm.isTetheringSupported() && mEm != null;
-        assumeTrue(mRunTests);
-
         mHandlerThread = new HandlerThread(getClass().getSimpleName());
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
+
+        mRunTests = isEthernetTetheringSupported();
+        assumeTrue(mRunTests);
+
         mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
     }
 
@@ -201,7 +230,6 @@
             mHandler.post(() -> reader.stop());
             mDownstreamReader = null;
         }
-        mHandlerThread.quitSafely();
         mTetheredInterfaceRequester.release();
         mEm.setIncludeTestInterfaces(false);
         maybeDeleteTestInterface();
@@ -212,6 +240,7 @@
         try {
             if (mRunTests) cleanUp();
         } finally {
+            mHandlerThread.quitSafely();
             mUiAutomation.dropShellPermissionIdentity();
         }
     }
@@ -400,6 +429,23 @@
         // client, which is not possible in this test.
     }
 
+    private boolean isEthernetTetheringSupported() throws Exception {
+        final CompletableFuture<Boolean> future = new CompletableFuture<>();
+        final TetheringEventCallback callback = new TetheringEventCallback() {
+            @Override
+            public void onSupportedTetheringTypes(Set<Integer> supportedTypes) {
+                future.complete(supportedTypes.contains(TETHERING_ETHERNET));
+            }
+        };
+
+        try {
+            mTm.registerTetheringEventCallback(mHandler::post, callback);
+            return future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } finally {
+            mTm.unregisterTetheringEventCallback(callback);
+        }
+    }
+
     private static final class MyTetheringEventCallback implements TetheringEventCallback {
         private final TetheringManager mTm;
         private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
@@ -748,21 +794,27 @@
         }
     }
 
-    private TestNetworkTracker createTestUpstream(final List<LinkAddress> addresses)
-            throws Exception {
+    private TestNetworkTracker createTestUpstream(final List<LinkAddress> addresses,
+            final List<InetAddress> dnses) throws Exception {
         mTm.setPreferTestNetworks(true);
 
-        return initTestNetwork(mContext, addresses, TIMEOUT_MS);
+        final LinkProperties lp = new LinkProperties();
+        lp.setLinkAddresses(addresses);
+        lp.setDnsServers(dnses);
+        lp.setNat64Prefix(TEST_NAT64PREFIX);
+
+        return initTestNetwork(mContext, lp, TIMEOUT_MS);
     }
 
     @Test
-    public void testTestNetworkUpstream() throws Exception {
+    public void testIcmpv6Echo() throws Exception {
         assumeFalse(mEm.isAvailable());
 
         // MyTetheringEventCallback currently only support await first available upstream. Tethering
         // may select internet network as upstream if test network is not available and not be
         // preferred yet. Create test upstream network before enable tethering.
-        mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR, TEST_IP6_ADDR));
+        mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR, TEST_IP6_ADDR),
+                toList(TEST_IP4_DNS, TEST_IP6_DNS));
 
         mDownstreamIface = createTestInterface();
         mEm.setIncludeTestInterfaces(true);
@@ -777,7 +829,40 @@
                 mTetheringEventCallback.awaitUpstreamChanged());
 
         mDownstreamReader = makePacketReader(mDownstreamIface);
-        // TODO: do basic forwarding test here.
+        mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
+
+        runPing6Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader));
+    }
+
+    private void runPing6Test(TetheringTester tester, RemoteResponder remote) throws Exception {
+        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, let
+        // TetheringTester test ipv6 tethering connectivity before testing ipv6.
+        // TODO: move to a common place to avoid that every IPv6 test needs to call this function.
+        tester.waitForIpv6TetherConnectivityVerified();
+
+        TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString("1:2:3:4:5:6"),
+                true /* hasIpv6 */);
+        Inet6Address remoteIp6Addr = (Inet6Address) parseNumericAddress("2400:222:222::222");
+        ByteBuffer request = Ipv6Utils.buildEchoRequestPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv6Addr, remoteIp6Addr);
+        tester.sendPacket(request);
+
+        final byte[] echoRequest = remote.getNextMatchedPacket((p) -> {
+            Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+
+            return isIcmpv6Type(p, false /* hasEth */, ICMPV6_ECHO_REQUEST_TYPE);
+        });
+        assertNotNull("No icmpv6 echo request in upstream", echoRequest);
+
+        ByteBuffer reply = Ipv6Utils.buildEchoReplyPacket(remoteIp6Addr, tethered.ipv6Addr);
+        remote.sendPacket(reply);
+
+        final byte[] echoReply = tester.getNextMatchedPacket((p) -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+
+            return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ECHO_REPLY_TYPE);
+        });
+        assertNotNull("No icmpv6 echo reply in downstream", echoReply);
     }
 
     // Test network topology:
@@ -795,12 +880,11 @@
     // Used by public port and private port. Assume port 9876 has not been used yet before the
     // testing that public port and private port are the same in the testing. Note that NAT port
     // forwarding could be different between private port and public port.
+    // TODO: move to the start of test class.
     private static final short LOCAL_PORT = 9876;
     private static final short REMOTE_PORT = 433;
     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 =
@@ -811,19 +895,20 @@
             ByteBuffer.wrap(new byte[] { (byte) 0x9a, (byte) 0xbc });
 
     private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
-            @NonNull final ByteBuffer payload) {
+            boolean isIpv4, @NonNull final ByteBuffer payload) {
         final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
 
         if (hasEther) {
-            final EthernetHeader etherHeader = Struct.parse(EthernetHeader.class, buf);
-            if (etherHeader == null) return false;
+            if (Struct.parse(EthernetHeader.class, buf) == null) return false;
         }
 
-        final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
-        if (ipv4Header == null) return false;
+        if (isIpv4) {
+            if (Struct.parse(Ipv4Header.class, buf) == null) return false;
+        } else {
+            if (Struct.parse(Ipv6Header.class, buf) == null) return false;
+        }
 
-        final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
-        if (udpHeader == null) return false;
+        if (Struct.parse(UdpHeader.class, buf) == null) return false;
 
         if (buf.remaining() != payload.limit()) return false;
 
@@ -832,21 +917,47 @@
     }
 
     @NonNull
-    private ByteBuffer buildUdpv4Packet(@Nullable final MacAddress srcMac,
-            @Nullable final MacAddress dstMac, short id,
-            @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp,
+    private ByteBuffer buildUdpPacket(
+            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+            @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
             short srcPort, short dstPort, @Nullable final ByteBuffer payload)
             throws Exception {
+        int ipProto;
+        short ethType;
+        if (srcIp instanceof Inet4Address && dstIp instanceof Inet4Address) {
+            ipProto = IPPROTO_IP;
+            ethType = (short) ETHER_TYPE_IPV4;
+        } else if (srcIp instanceof Inet6Address && dstIp instanceof Inet6Address) {
+            ipProto = IPPROTO_IPV6;
+            ethType = (short) ETHER_TYPE_IPV6;
+        } else {
+            fail("Unsupported conditions: srcIp " + srcIp + ", dstIp " + dstIp);
+            // Make compiler happy to the uninitialized ipProto and ethType.
+            return null;  // unreachable, the annotation @NonNull of function return value is true.
+        }
+
         final boolean hasEther = (srcMac != null && dstMac != null);
         final int payloadLen = (payload == null) ? 0 : payload.limit();
-        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, IPPROTO_IP, IPPROTO_UDP,
+        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, ipProto, IPPROTO_UDP,
                 payloadLen);
         final PacketBuilder packetBuilder = new PacketBuilder(buffer);
 
+        // [1] Ethernet header
         if (hasEther) packetBuilder.writeL2Header(srcMac, dstMac, (short) ETHER_TYPE_IPV4);
-        packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
-                TIME_TO_LIVE, (byte) IPPROTO_UDP, srcIp, dstIp);
+
+        // [2] IP header
+        if (ipProto == IPPROTO_IP) {
+            packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                    TIME_TO_LIVE, (byte) IPPROTO_UDP, (Inet4Address) srcIp, (Inet4Address) dstIp);
+        } else {
+            packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL, (byte) IPPROTO_UDP,
+                    HOP_LIMIT, (Inet6Address) srcIp, (Inet6Address) dstIp);
+        }
+
+        // [3] UDP header
         packetBuilder.writeUdpHeader(srcPort, dstPort);
+
+        // [4] Payload
         if (payload != null) {
             buffer.put(payload);
             // in case data might be reused by caller, restore the position and
@@ -858,10 +969,10 @@
     }
 
     @NonNull
-    private ByteBuffer buildUdpv4Packet(short id, @NonNull final Inet4Address srcIp,
-            @NonNull final Inet4Address dstIp, short srcPort, short dstPort,
+    private ByteBuffer buildUdpPacket(@NonNull final InetAddress srcIp,
+            @NonNull final InetAddress dstIp, short srcPort, short dstPort,
             @Nullable final ByteBuffer payload) throws Exception {
-        return buildUdpv4Packet(null /* srcMac */, null /* dstMac */, id, srcIp, dstIp, srcPort,
+        return buildUdpPacket(null /* srcMac */, null /* dstMac */, srcIp, dstIp, srcPort,
                 dstPort, payload);
     }
 
@@ -869,9 +980,9 @@
     // See #runUdp4Test.
     private boolean isIpv4TetherConnectivityVerified(TetheringTester tester,
             RemoteResponder remote, TetheredDevice tethered) throws Exception {
-        final ByteBuffer probePacket = buildUdpv4Packet(tethered.macAddr,
-                tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
-                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
+        final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
                 TEST_REACHABILITY_PAYLOAD);
 
         // Send a UDP packet from client and check the packet can be found on upstream interface.
@@ -879,7 +990,8 @@
             tester.sendPacket(probePacket);
             byte[] expectedPacket = remote.getNextMatchedPacket(p -> {
                 Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-                return isExpectedUdpPacket(p, false /* hasEther */, TEST_REACHABILITY_PAYLOAD);
+                return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */,
+                        TEST_REACHABILITY_PAYLOAD);
             });
             if (expectedPacket != null) return true;
         }
@@ -889,7 +1001,7 @@
     private void runUdp4Test(TetheringTester tester, RemoteResponder remote, boolean usingBpf)
             throws Exception {
         final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
-                "1:2:3:4:5:6"));
+                "1:2:3:4:5:6"), false /* hasIpv6 */);
 
         // TODO: remove the connectivity verification for upstream connected notification race.
         // Because async upstream connected notification can't guarantee the tethering routing is
@@ -900,23 +1012,23 @@
         assertTrue(isIpv4TetherConnectivityVerified(tester, remote, tethered));
 
         // Send a UDP packet in original direction.
-        final ByteBuffer originalPacket = buildUdpv4Packet(tethered.macAddr,
-                tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
-                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
+        final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
                 PAYLOAD /* payload */);
         tester.verifyUpload(remote, originalPacket, p -> {
             Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-            return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD);
+            return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
         });
 
         // Send a UDP packet in reply direction.
         final Inet4Address publicIp4Addr = (Inet4Address) TEST_IP4_ADDR.getAddress();
-        final ByteBuffer replyPacket = buildUdpv4Packet(ID2, REMOTE_IP4_ADDR /* srcIp */,
-                publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /*dstPort */,
+        final ByteBuffer replyPacket = buildUdpPacket(REMOTE_IP4_ADDR /* srcIp */,
+                publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
                 PAYLOAD2 /* payload */);
         remote.verifyDownload(tester, replyPacket, p -> {
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
-            return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
+            return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
         });
 
         if (usingBpf) {
@@ -929,46 +1041,89 @@
             // 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 */,
+            final ByteBuffer originalPacket2 = buildUdpPacket(tethered.macAddr,
+                    tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                     REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */,
-                    REMOTE_PORT /*dstPort */, PAYLOAD3 /* payload */);
+                    REMOTE_PORT /* dstPort */, PAYLOAD3 /* payload */);
             tester.verifyUpload(remote, originalPacket2, p -> {
                 Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-                return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD3);
+                return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD3);
             });
 
-            final HashMap<Tether4Key, Tether4Value> upstreamMap = pollIpv4UpstreamMapFromDump();
+            // [1] Verify IPv4 upstream rule map.
+            final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
+                    Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
             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 Tether4Key upstream4Key = rule.getKey();
+            assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
+            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
+            assertEquals(LOCAL_PORT, upstream4Key.srcPort);
+            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
+            assertEquals(REMOTE_PORT, upstream4Key.dstPort);
 
-            final Tether4Value value = rule.getValue();
+            final Tether4Value upstream4Value = rule.getValue();
             assertTrue(Arrays.equals(publicIp4Addr.getAddress(),
-                    InetAddress.getByAddress(value.src46).getAddress()));
-            assertEquals(LOCAL_PORT, value.srcPort);
+                    InetAddress.getByAddress(upstream4Value.src46).getAddress()));
+            assertEquals(LOCAL_PORT, upstream4Value.srcPort);
             assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
-                    InetAddress.getByAddress(value.dst46).getAddress()));
-            assertEquals(REMOTE_PORT, value.dstPort);
+                    InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
+            assertEquals(REMOTE_PORT, upstream4Value.dstPort);
+
+            // [2] Verify stats map.
+            // Transmit packets on both direction for verifying stats. Because we only care the
+            // packet count in stats test, we just reuse the existing packets to increaes
+            // the packet count on both direction.
+
+            // Send packets on original direction.
+            for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+                tester.verifyUpload(remote, originalPacket, p -> {
+                    Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+                    return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
+                });
+            }
+
+            // Send packets on reply direction.
+            for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+                remote.verifyDownload(tester, replyPacket, p -> {
+                    Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+                    return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
+                });
+            }
+
+            // Dump stats map to verify.
+            final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
+                    TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+            assertNotNull(statsMap);
+            assertEquals(1, statsMap.size());
+
+            final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
+                    statsMap.entrySet().iterator().next();
+
+            // TODO: verify the upstream index in TetherStatsKey.
+
+            final TetherStatsValue statsValue = stats.getValue();
+            assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
+            assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
+            assertEquals(0, statsValue.rxErrors);
+            assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
+            assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
+            assertEquals(0, statsValue.txErrors);
         }
     }
 
-    void initializeTethering() throws Exception {
+    void initializeTethering(List<LinkAddress> upstreamAddresses, List<InetAddress> upstreamDnses)
+            throws Exception {
         assumeFalse(mEm.isAvailable());
 
         // MyTetheringEventCallback currently only support await first available upstream. Tethering
         // may select internet network as upstream if test network is not available and not be
         // preferred yet. Create test upstream network before enable tethering.
-        mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR));
+        mUpstreamTracker = createTestUpstream(upstreamAddresses, upstreamDnses);
 
         mDownstreamIface = createTestInterface();
         mEm.setIncludeTestInterfaces(true);
@@ -987,23 +1142,58 @@
     }
 
     @Test
-    @IgnoreAfter(Build.VERSION_CODES.Q)
-    public void testTetherUdpV4WithoutBpf() throws Exception {
-        initializeTethering();
+    @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testTetherUdpV4UpToR() throws Exception {
+        initializeTethering(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS));
         runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
                 false /* usingBpf */);
     }
 
+    private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
+        final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
+        return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
+                || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
+                || current.isAtLeast(new KVersion(5, 4, 98));
+    }
+
+    @Test
+    public void testIsUdpOffloadSupportedByKernel() throws Exception {
+        assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
+        assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
+
+        assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
+        assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
+
+        assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
+    }
+
+    // TODO: refactor test testTetherUdpV4* into IPv4 UDP non-offload and offload tests.
+    // That can be easier to know which feature is verified from test results.
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.R)
-    public void testTetherUdpV4WithBpf() throws Exception {
-        initializeTethering();
+    public void testTetherUdpV4AfterR() throws Exception {
+        initializeTethering(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS));
+        final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
+        boolean usingBpf = isUdpOffloadSupportedByKernel(kernelVersion);
+        if (!usingBpf) {
+            Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test for kernel "
+                    + kernelVersion);
+        }
         runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
-                true /* usingBpf */);
+                usingBpf);
     }
 
     @Nullable
-    private Pair<Tether4Key, Tether4Value> parseTether4KeyValue(@NonNull String dumpStr) {
+    private <K extends Struct, V extends Struct> Pair<K, V> parseMapKeyValue(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String dumpStr) {
         Log.w(TAG, "Parsing string: " + dumpStr);
 
         String[] keyValueStrs = dumpStr.split(BASE64_DELIMITER);
@@ -1016,36 +1206,38 @@
         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 K k = Struct.parse(keyClass, keyByteBuffer);
 
         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);
+        final V v = Struct.parse(valueClass, valueByteBuffer);
 
-        return new Pair<>(tether4Key, tether4Value);
+        return new Pair<>(k, v);
     }
 
     @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<>();
+    private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
+        final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
+        final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args);
+        final HashMap<K, V> map = new HashMap<>();
 
         for (final String line : rawMapStr.split(LINE_DELIMITER)) {
-            final Pair<Tether4Key, Tether4Value> rule = parseTether4KeyValue(line.trim());
+            final Pair<K, V> rule = parseMapKeyValue(keyClass, valueClass, line.trim());
             map.put(rule.first, rule.second);
         }
         return map;
     }
 
     @Nullable
-    private HashMap<Tether4Key, Tether4Value> pollIpv4UpstreamMapFromDump() throws Exception {
+    private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
         for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
-            final HashMap<Tether4Key, Tether4Value> map = dumpIpv4UpstreamMap();
+            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
             if (!map.isEmpty()) return map;
 
             Thread.sleep(DUMP_POLLING_INTERVAL_MS);
@@ -1055,6 +1247,96 @@
         return null;
     }
 
+    @Nullable
+    private Inet6Address getClatIpv6Address(TetheringTester tester,
+            RemoteResponder remote, TetheredDevice tethered) throws Exception {
+        final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
+                TEST_REACHABILITY_PAYLOAD);
+
+        // Send an IPv4 UDP packet from client and check that a CLAT translated IPv6 UDP packet can
+        // be found on upstream interface. Get CLAT IPv6 address from the CLAT translated IPv6 UDP
+        // packet.
+        byte[] expectedPacket = null;
+        for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
+            tester.sendPacket(probePacket);
+            expectedPacket = remote.getNextMatchedPacket(p -> {
+                Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+                return isExpectedUdpPacket(p, false /* hasEther */, false /* isIpv4 */,
+                        TEST_REACHABILITY_PAYLOAD);
+            });
+            if (expectedPacket != null) break;
+        }
+        if (expectedPacket == null) return null;
+
+        // Above has guaranteed that the found packet is an IPv6 packet without ether header.
+        final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class,
+                ByteBuffer.wrap(expectedPacket));
+        return ipv6Header.srcIp;
+    }
+
+    // Test network topology:
+    //
+    //            public network (rawip)                 private network
+    //                      |         UE (CLAT support)         |
+    // +---------------+    V    +------------+------------+    V    +------------+
+    // | NAT64 Gateway +---------+  Upstream  | Downstream +---------+   Client   |
+    // +---------------+         +------------+------------+         +------------+
+    // remote ip                 public ip                           private ip
+    // [64:ff9b::808:808]:443    [clat ipv6]:9876                    [TetheredDevice ipv4]:9876
+    //
+    // Note that CLAT IPv6 address is generated by ClatCoordinator. Get the CLAT IPv6 address by
+    // sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
+    // packet.
+    //
+    private void runClatUdpTest(TetheringTester tester, RemoteResponder remote)
+            throws Exception {
+        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, let
+        // TetheringTester test ipv6 tethering connectivity before testing ipv6.
+        // TODO: move to a common place to avoid that every IPv6 test needs to call this function.
+        tester.waitForIpv6TetherConnectivityVerified();
+
+        final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
+                "1:2:3:4:5:6"), true /* hasIpv6 */);
+
+        // Get CLAT IPv6 address.
+        final Inet6Address clatAddr6 = getClatIpv6Address(tester, remote, tethered);
+        assertNotNull(clatAddr6);
+
+        // Send an IPv4 UDP packet in original direction.
+        // IPv4 packet -- CLAT translation --> IPv6 packet
+        final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
+                PAYLOAD /* payload */);
+        tester.verifyUpload(remote, originalPacket, p -> {
+            Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+            return isExpectedUdpPacket(p, false /* hasEther */, false /* isIpv4 */, PAYLOAD);
+        });
+
+        // Send an IPv6 UDP packet in reply direction.
+        // IPv6 packet -- CLAT translation --> IPv4 packet
+        final ByteBuffer replyPacket = buildUdpPacket(REMOTE_NAT64_ADDR /* srcIp */,
+                clatAddr6 /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
+                PAYLOAD2 /* payload */);
+        remote.verifyDownload(tester, replyPacket, p -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+            return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
+        });
+
+        // TODO: test CLAT bpf maps.
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testTetherClatUdp() throws Exception {
+        // CLAT only starts on IPv6 only network.
+        initializeTethering(toList(TEST_IP6_ADDR), toList(TEST_IP6_DNS));
+        runClatUdpTest(new TetheringTester(mDownstreamReader),
+                new RemoteResponder(mUpstreamReader));
+    }
+
     private <T> List<T> toList(T... array) {
         return Arrays.asList(array);
     }
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java
index d24661a..458680a 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -16,10 +16,22 @@
 
 package android.net;
 
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
 import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
@@ -32,11 +44,24 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.NsHeader;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
 import com.android.networkstack.arp.ArpPacket;
 import com.android.testutils.TapPacketReader;
 
 import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Random;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Predicate;
@@ -50,12 +75,14 @@
     private static final String TAG = TetheringTester.class.getSimpleName();
     private static final int PACKET_READ_TIMEOUT_MS = 100;
     private static final int DHCP_DISCOVER_ATTEMPTS = 10;
+    private static final int READ_RA_ATTEMPTS = 10;
     private static final byte[] DHCP_REQUESTED_PARAMS = new byte[] {
             DhcpPacket.DHCP_SUBNET_MASK,
             DhcpPacket.DHCP_ROUTER,
             DhcpPacket.DHCP_DNS_SERVER,
             DhcpPacket.DHCP_LEASE_TIME,
     };
+    private static final InetAddress LINK_LOCAL = parseNumericAddress("fe80::1");
 
     public static final String DHCP_HOSTNAME = "testhostname";
 
@@ -69,12 +96,13 @@
         mTetheredDevices = new ArrayMap<>();
     }
 
-    public TetheredDevice createTetheredDevice(MacAddress macAddr) throws Exception {
+    public TetheredDevice createTetheredDevice(MacAddress macAddr, boolean hasIpv6)
+            throws Exception {
         if (mTetheredDevices.get(macAddr) != null) {
             fail("Tethered device already created");
         }
 
-        TetheredDevice tethered = new TetheredDevice(macAddr);
+        TetheredDevice tethered = new TetheredDevice(macAddr, hasIpv6);
         mTetheredDevices.put(macAddr, tethered);
 
         return tethered;
@@ -84,14 +112,15 @@
         public final MacAddress macAddr;
         public final MacAddress routerMacAddr;
         public final Inet4Address ipv4Addr;
+        public final Inet6Address ipv6Addr;
 
-        private TetheredDevice(MacAddress mac) throws Exception {
+        private TetheredDevice(MacAddress mac, boolean hasIpv6) throws Exception {
             macAddr = mac;
-
             DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
             ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
             routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
                     dhcpResults.serverAddress);
+            ipv6Addr = hasIpv6 ? runSlaac(macAddr, routerMacAddr) : null;
         }
     }
 
@@ -216,6 +245,141 @@
         return null;
     }
 
+    public void waitForIpv6TetherConnectivityVerified() throws Exception {
+        Log.d(TAG, "Waiting RA multicast");
+
+        // Wait for RA multicast message from router to confirm that the IPv6 tethering
+        // connectivity is ready. We don't extract the router mac address from RA because
+        // we get the router mac address from IPv4 ARP packet. See #getRouterMacAddressFromArp.
+        for (int i = 0; i < READ_RA_ATTEMPTS; i++) {
+            final byte[] raPacket = getNextMatchedPacket((p) -> {
+                return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
+            });
+            if (raPacket != null) return;
+        }
+
+        fail("Could not get RA multicast packet after " + READ_RA_ATTEMPTS + " attempts");
+    }
+
+    private List<PrefixInformationOption> getRaPrefixOptions(byte[] packet) {
+        ByteBuffer buf = ByteBuffer.wrap(packet);
+        if (!isIcmpv6Type(buf, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return null;
+
+        Struct.parse(RaHeader.class, buf);
+        final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
+        while (buf.position() < packet.length) {
+            final int currentPos = buf.position();
+            final int type = Byte.toUnsignedInt(buf.get());
+            final int length = Byte.toUnsignedInt(buf.get());
+            if (type == ICMPV6_ND_OPTION_PIO) {
+                final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos,
+                        Struct.getSize(PrefixInformationOption.class));
+                final PrefixInformationOption pio =
+                        Struct.parse(PrefixInformationOption.class, pioBuf);
+                pioList.add(pio);
+
+                // Move ByteBuffer position to the next option.
+                buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+            } else {
+                buf.position(currentPos + (length * 8));
+            }
+        }
+        return pioList;
+    }
+
+    private Inet6Address runSlaac(MacAddress srcMac, MacAddress dstMac) throws Exception {
+        sendRsPacket(srcMac, dstMac);
+
+        final byte[] raPacket = getNextMatchedPacket((p) -> {
+            return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
+        });
+
+        if (raPacket == null) {
+            fail("Could not get ra for prefix options");
+        }
+        final List<PrefixInformationOption> options = getRaPrefixOptions(raPacket);
+
+        for (PrefixInformationOption pio : options) {
+            if (pio.validLifetime > 0) {
+                final byte[] addressBytes = pio.prefix;
+                // Random the last two bytes as suffix.
+                // TODO: Currently do not implmement DAD in the test. Rely the gateway ipv6 address
+                // genetrated by tethering module always has random the last byte.
+                addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
+                addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
+
+                return (Inet6Address) InetAddress.getByAddress(addressBytes);
+            }
+        }
+
+        fail("Could not get ipv6 address");
+        return null;
+    }
+
+    private void sendRsPacket(MacAddress srcMac, MacAddress dstMac) throws Exception {
+        Log.d(TAG, "Sending RS");
+        ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+        ByteBuffer rs = Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) LINK_LOCAL,
+                IPV6_ADDR_ALL_NODES_MULTICAST, slla);
+
+        sendPacket(rs);
+    }
+
+    private void maybeReplyNa(byte[] packet) {
+        ByteBuffer buf = ByteBuffer.wrap(packet);
+        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+        if (ethHdr.etherType != ETHER_TYPE_IPV6) return;
+
+        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+        if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return;
+
+        final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+        if (icmpv6Hdr.type != (short) ICMPV6_NEIGHBOR_SOLICITATION) return;
+
+        final NsHeader nsHdr = Struct.parse(NsHeader.class, buf);
+        for (int i = 0; i < mTetheredDevices.size(); i++) {
+            TetheredDevice tethered = mTetheredDevices.valueAt(i);
+            if (!nsHdr.target.equals(tethered.ipv6Addr)) continue;
+
+            final ByteBuffer tlla = LlaOption.build((byte) ICMPV6_ND_OPTION_TLLA, tethered.macAddr);
+            int flags = NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
+                    | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+            ByteBuffer ns = Ipv6Utils.buildNaPacket(tethered.macAddr, tethered.routerMacAddr,
+                    nsHdr.target, ipv6Hdr.srcIp, flags, nsHdr.target, tlla);
+            try {
+                sendPacket(ns);
+            } catch (Exception e) {
+                fail("Failed to reply NA for " + tethered.ipv6Addr);
+            }
+
+            return;
+        }
+    }
+
+    public static boolean isIcmpv6Type(byte[] packet, boolean hasEth, int type) {
+        final ByteBuffer buf = ByteBuffer.wrap(packet);
+        return isIcmpv6Type(buf, hasEth, type);
+    }
+
+    private static boolean isIcmpv6Type(ByteBuffer buf, boolean hasEth, int type) {
+        try {
+            if (hasEth) {
+                final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+                if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
+            }
+
+            final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+            if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
+
+            final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+            return icmpv6Hdr.type == (short) type;
+        } catch (Exception e) {
+            // Parsing packet fail means it is not icmpv6 packet.
+        }
+
+        return false;
+    }
+
     public void sendPacket(ByteBuffer packet) throws Exception {
         mDownstreamReader.sendResponse(packet);
     }
@@ -226,6 +390,7 @@
             if (filter.test(packet)) return packet;
 
             maybeReplyArp(packet);
+            maybeReplyNa(packet);
         }
 
         return null;
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/Android.bp b/Tethering/tests/mts/Android.bp
index 18fd63b..a84fdd2 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -22,7 +22,7 @@
     name: "MtsTetheringTestLatestSdk",
 
     min_sdk_version: "30",
-    target_sdk_version: "31",
+    target_sdk_version: "33",
 
     libs: [
         "android.test.base",
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index ad2faa0..68c1c57 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -352,15 +352,6 @@
         assertFalse(mTestMap.isEmpty());
         mTestMap.clear();
         assertTrue(mTestMap.isEmpty());
-
-        // Clearing an already-closed map throws.
-        mTestMap.close();
-        try {
-            mTestMap.clear();
-            fail("clearing already-closed map should throw");
-        } catch (ErrnoException expected) {
-            assertEquals(OsConstants.EBADF, expected.errno);
-        }
     }
 
     @Test
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index d1b8380..0ee12ad 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -89,7 +89,7 @@
     static_libs: [
         "TetheringApiStableLib",
     ],
-    target_sdk_version: "31",
+    target_sdk_version: "33",
     visibility: [
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 6488421..bf7e887 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -103,6 +103,8 @@
 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,10 +114,9 @@
 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.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -156,6 +157,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 */);
@@ -184,6 +187,7 @@
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
+    @Mock private TetheringMetrics mTetheringMetrics;
     @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
     @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
     @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
@@ -230,9 +234,10 @@
 
         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, mTetherConfig, mAddressCoordinator, mDependencies);
+                mCallback, mTetherConfig, mAddressCoordinator, mTetheringMetrics, mDependencies);
         mIpServer.start();
         mNeighborEventConsumer = neighborCaptor.getValue();
 
@@ -364,7 +369,7 @@
                 .thenReturn(mIpNeighborMonitor);
         mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
                 mNetd, mBpfCoordinator, mCallback, mTetherConfig, mAddressCoordinator,
-                mDependencies);
+                mTetheringMetrics, mDependencies);
         mIpServer.start();
         mLooper.dispatchAll();
         verify(mCallback).updateInterfaceState(
@@ -448,6 +453,9 @@
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), any(LinkProperties.class));
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_BLUETOOTH),
+                eq(TETHER_ERROR_NO_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_BLUETOOTH));
         verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
     }
 
@@ -655,6 +663,9 @@
         usbTeardownOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), mLinkPropertiesCaptor.capture());
         assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_USB),
+                eq(TETHER_ERROR_TETHER_IFACE_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_USB));
     }
 
     @Test
@@ -673,6 +684,9 @@
         usbTeardownOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), mLinkPropertiesCaptor.capture());
         assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_USB),
+                eq(TETHER_ERROR_ENABLE_FORWARDING_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_USB));
     }
 
     @Test
@@ -1312,6 +1326,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 179fc8a..3630f24 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -102,6 +102,8 @@
 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;
@@ -224,8 +226,6 @@
             private int mSrcPort = PRIVATE_PORT;
             private int mDstPort = REMOTE_PORT;
 
-            Builder() {}
-
             public Builder setProto(int proto) {
                 if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
                     fail("Not support protocol " + proto);
@@ -250,8 +250,6 @@
             private int mSrcPort = REMOTE_PORT;
             private int mDstPort = PUBLIC_PORT;
 
-            Builder() {}
-
             public Builder setProto(int proto) {
                 if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
                     fail("Not support protocol " + proto);
@@ -279,8 +277,6 @@
             private int mDstPort = REMOTE_PORT;
             private long mLastUsed = 0;
 
-            Builder() {}
-
             public Tether4Value build() {
                 return new Tether4Value(mOif, mEthDstMac, mEthSrcMac, mEthProto, mPmtu,
                         mSrc46, mDst46, mSrcPort, mDstPort, mLastUsed);
@@ -301,8 +297,6 @@
             private int mDstPort = PRIVATE_PORT;
             private long mLastUsed = 0;
 
-            Builder() {}
-
             public Tether4Value build() {
                 return new Tether4Value(mOif, mEthDstMac, mEthSrcMac, mEthProto, mPmtu,
                         mSrc46, mDst46, mSrcPort, mDstPort, mLastUsed);
@@ -321,8 +315,6 @@
             private short mPublicPort = PUBLIC_PORT;
             private short mRemotePort = REMOTE_PORT;
 
-            Builder() {}
-
             public Builder setMsgType(short msgType) {
                 if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) {
                     fail("Not support message type " + msgType);
@@ -377,6 +369,7 @@
     // 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 =
@@ -482,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);
@@ -1793,4 +1788,280 @@
             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 690ff71..01d7b4b 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -304,33 +304,6 @@
     }
 
     @Test
-    public void toleratesCarrierConfigManagerMissing() {
-        setupForRequiredProvisioning();
-        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.
-        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
-    }
-
-    @Test
-    public void toleratesCarrierConfigMissing() {
-        setupForRequiredProvisioning();
-        when(mCarrierConfigManager.getConfig()).thenReturn(null);
-        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
-        // We still have a provisioning app configured, so still require provisioning.
-        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
-    }
-
-    @Test
-    public void toleratesCarrierConfigNotLoaded() {
-        setupForRequiredProvisioning();
-        mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, false);
-        // We still have a provisioning app configured, so still require provisioning.
-        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
-    }
-
-    @Test
     public void provisioningNotRequiredWhenAppNotFound() {
         setupForRequiredProvisioning();
         when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
@@ -706,8 +679,8 @@
     @IgnoreUpTo(SC_V2)
     public void requestLatestTetheringEntitlementResult_carrierDoesNotSupport_noProvisionCount()
             throws Exception {
-        setupForRequiredProvisioning();
         setupCarrierConfig(false);
+        setupForRequiredProvisioning();
         mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         ResultReceiver receiver = new ResultReceiver(null) {
             @Override
@@ -735,6 +708,7 @@
         mEnMgr.notifyUpstream(false);
         mLooper.dispatchAll();
         setupCarrierConfig(false);
+        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         mEnMgr.reevaluateSimCardProvisioning(mConfig);
 
         // Turn on upstream.
@@ -749,8 +723,8 @@
     @IgnoreUpTo(SC_V2)
     public void startProvisioningIfNeeded_carrierUnsupport()
             throws Exception {
-        setupForRequiredProvisioning();
         setupCarrierConfig(false);
+        setupForRequiredProvisioning();
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
         verify(mTetherProvisioningFailedListener, never())
                 .onTetherProvisioningFailed(TETHERING_WIFI, "Carrier does not support.");
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 e9716b3..8ef0c76 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -26,6 +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;
@@ -668,7 +669,7 @@
 
         if (isAtLeastT()) {
             mTetherStatsProviderCb.expectNotifyLimitReached();
-        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
+        } else if (isAtLeastS()) {
             mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
         } else {
             mTetherStatsProviderCb.expectNotifyLimitReached();
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 e8bb315..3190f35 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -22,10 +22,13 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL;
+import static android.telephony.CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
 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.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_NCM_FUNCTION;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_RNDIS_FUNCTION;
@@ -46,8 +49,10 @@
 import android.content.res.Resources;
 import android.net.util.SharedLog;
 import android.os.Build;
+import android.os.PersistableBundle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
 import android.telephony.TelephonyManager;
 import android.test.mock.MockContentResolver;
 
@@ -56,6 +61,7 @@
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -88,6 +94,7 @@
     private static final long TEST_PACKAGE_VERSION = 1234L;
     @Mock private ApplicationInfo mApplicationInfo;
     @Mock private Context mContext;
+    @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private TelephonyManager mTelephonyManager;
     @Mock private Resources mResources;
     @Mock private Resources mResourcesForSubId;
@@ -97,6 +104,7 @@
     private boolean mHasTelephonyManager;
     private MockitoSession mMockingSession;
     private MockContentResolver mContentResolver;
+    private final PersistableBundle mCarrierConfig = new PersistableBundle();
 
     private class MockTetheringConfiguration extends TetheringConfiguration {
         MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
@@ -474,6 +482,56 @@
                 PROVISIONING_APP_RESPONSE);
     }
 
+    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 testGetCarrierConfigBySubId_noCarrierConfigManager_configsAreDefault() {
+        // Act like the CarrierConfigManager is present and ready unless told otherwise.
+        mockService(Context.CARRIER_CONFIG_SERVICE,
+                CarrierConfigManager.class, null);
+        final TetheringConfiguration cfg = new TetheringConfiguration(
+                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+        assertTrue(cfg.isCarrierSupportTethering);
+        assertTrue(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+    }
+
+    @Test
+    public void testGetCarrierConfigBySubId_carrierConfigMissing_configsAreDefault() {
+        // Act like the CarrierConfigManager is present and ready unless told otherwise.
+        mockService(Context.CARRIER_CONFIG_SERVICE,
+                CarrierConfigManager.class, mCarrierConfigManager);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(null);
+        final TetheringConfiguration cfg = new TetheringConfiguration(
+                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+        assertTrue(cfg.isCarrierSupportTethering);
+        assertTrue(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+    }
+
+    @Test
+    public void testGetCarrierConfigBySubId_hasConfigs_carrierUnsupportAndCheckNotRequired() {
+        mockService(Context.CARRIER_CONFIG_SERVICE,
+                CarrierConfigManager.class, mCarrierConfigManager);
+        mCarrierConfig.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
+        mCarrierConfig.putBoolean(KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
+        mCarrierConfig.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
+        final TetheringConfiguration cfg = new TetheringConfiguration(
+                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+        if (SdkLevel.isAtLeastT()) {
+            assertFalse(cfg.isCarrierSupportTethering);
+        } else {
+            assertTrue(cfg.isCarrierSupportTethering);
+        }
+        assertFalse(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+
+    }
+
     @Test
     public void testEnableLegacyWifiP2PAddress() throws Exception {
         final TetheringConfiguration defaultCfg = new TetheringConfiguration(
@@ -616,4 +674,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/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index f664d5d..9db8f16 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -275,7 +275,7 @@
         mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
                 result);
         verify(mTethering).isTetheringSupported();
-        verify(mTethering).startTethering(eq(request), eq(result));
+        verify(mTethering).startTethering(eq(request), eq(TEST_CALLER_PKG), eq(result));
     }
 
     @Test
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 0388758..773cae3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -45,6 +45,7 @@
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
@@ -57,6 +58,7 @@
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
 import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -142,6 +144,7 @@
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
 import android.net.TetheringInterface;
+import android.net.TetheringManager;
 import android.net.TetheringRequestParcel;
 import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
@@ -175,6 +178,7 @@
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
 import android.test.mock.MockContentResolver;
+import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
@@ -190,11 +194,15 @@
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.MiscAsserts;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -211,11 +219,14 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.Vector;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class TetheringTest {
+    @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final int IFINDEX_OFFSET = 100;
 
     private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
@@ -236,6 +247,7 @@
     private static final String TEST_WIFI_REGEX = "test_wlan\\d";
     private static final String TEST_P2P_REGEX = "test_p2p-p2p\\d-.*";
     private static final String TEST_BT_REGEX = "test_pan\\d";
+    private static final String TEST_CALLER_PKG = "com.test.tethering";
 
     private static final int CELLULAR_NETID = 100;
     private static final int WIFI_NETID = 101;
@@ -270,6 +282,7 @@
     @Mock private BluetoothPan mBluetoothPan;
     @Mock private BluetoothPanShim mBluetoothPanShim;
     @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
+    @Mock private TetheringMetrics mTetheringMetrics;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -293,6 +306,7 @@
     private OffloadController mOffloadCtrl;
     private PrivateAddressCoordinator mPrivateAddressCoordinator;
     private SoftApCallback mSoftApCallback;
+    private SoftApCallback mLocalOnlyHotspotCallback;
     private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
     private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
 
@@ -494,6 +508,11 @@
         }
 
         @Override
+        public TetheringMetrics getTetheringMetrics() {
+            return mTetheringMetrics;
+        }
+
+        @Override
         public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
                 TetheringConfiguration cfg) {
             mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
@@ -658,6 +677,14 @@
         verify(mWifiManager).registerSoftApCallback(any(), softApCallbackCaptor.capture());
         mSoftApCallback = softApCallbackCaptor.getValue();
 
+        if (isAtLeastT()) {
+            final ArgumentCaptor<SoftApCallback> localOnlyCallbackCaptor =
+                    ArgumentCaptor.forClass(SoftApCallback.class);
+            verify(mWifiManager).registerLocalOnlyHotspotSoftApCallback(any(),
+                    localOnlyCallbackCaptor.capture());
+            mLocalOnlyHotspotCallback = localOnlyCallbackCaptor.getValue();
+        }
+
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
     }
@@ -852,7 +879,8 @@
 
     private void prepareNcmTethering() {
         // Emulate startTethering(TETHERING_NCM) called
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
     }
@@ -860,7 +888,7 @@
     private void prepareUsbTethering() {
         // Emulate pressing the USB tethering button in Settings UI.
         final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
-        mTethering.startTethering(request, null);
+        mTethering.startTethering(request, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
 
         assertEquals(1, mTethering.getActiveTetheringRequests().size());
@@ -933,7 +961,7 @@
 
         // Emulate externally-visible WifiManager effects, when hotspot mode
         // is being torn down.
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
@@ -1430,7 +1458,8 @@
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
@@ -1457,7 +1486,8 @@
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
@@ -1506,7 +1536,7 @@
 
         // Emulate externally-visible WifiManager effects, when tethering mode
         // is being torn down.
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
@@ -1533,11 +1563,13 @@
         doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
         verifyNoMoreInteractions(mNetd);
+        verify(mTetheringMetrics).createBuilder(eq(TETHERING_WIFI), anyString());
 
         // Emulate externally-visible WifiManager effects, causing the
         // per-interface state machine to start up, and telling us that
@@ -1576,6 +1608,10 @@
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
 
+        verify(mTetheringMetrics, times(2)).updateErrorCode(eq(TETHERING_WIFI),
+                eq(TETHER_ERROR_INTERNAL_ERROR));
+        verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_WIFI));
+
         verifyNoMoreInteractions(mWifiManager);
         verifyNoMoreInteractions(mNetd);
     }
@@ -1696,6 +1732,7 @@
         private final ArrayList<TetherStatesParcel> mTetherStates = new ArrayList<>();
         private final ArrayList<Integer> mOffloadStatus = new ArrayList<>();
         private final ArrayList<List<TetheredClient>> mTetheredClients = new ArrayList<>();
+        private final ArrayList<Long> mSupportedBitmaps = new ArrayList<>();
 
         // This function will remove the recorded callbacks, so it must be called once for
         // each callback. If this is called after multiple callback, the order matters.
@@ -1748,6 +1785,10 @@
             assertTrue(leases.containsAll(result));
         }
 
+        public void expectSupportedTetheringTypes(Set<Integer> expectedTypes) {
+            assertEquals(expectedTypes, TetheringManager.unpackBits(mSupportedBitmaps.remove(0)));
+        }
+
         @Override
         public void onUpstreamChanged(Network network) {
             mActualUpstreams.add(network);
@@ -1780,11 +1821,17 @@
             mTetherStates.add(parcel.states);
             mOffloadStatus.add(parcel.offloadStatus);
             mTetheredClients.add(parcel.tetheredClients);
+            mSupportedBitmaps.add(parcel.supportedTypes);
         }
 
         @Override
         public void onCallbackStopped(int errorCode) { }
 
+        @Override
+        public void onSupportedTetheringTypes(long supportedBitmap) {
+            mSupportedBitmaps.add(supportedBitmap);
+        }
+
         public void assertNoUpstreamChangeCallback() {
             assertTrue(mActualUpstreams.isEmpty());
         }
@@ -1867,7 +1914,8 @@
         tetherState = callback.pollTetherStatesChanged();
         assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
 
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         tetherState = callback.pollTetherStatesChanged();
         assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
@@ -1889,7 +1937,13 @@
         mTethering.unregisterTetheringEventCallback(callback);
         mLooper.dispatchAll();
         mTethering.stopTethering(TETHERING_WIFI);
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED);
+        if (isAtLeastT()) {
+            // After T, tethering doesn't support WIFI_AP_STATE_DISABLED with null interface name.
+            callback2.assertNoStateChangeCallback();
+            sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
+                    IFACE_IP_MODE_TETHERED);
+        }
         tetherState = callback2.pollTetherStatesChanged();
         assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
         mLooper.dispatchAll();
@@ -1964,10 +2018,12 @@
     public void testNoDuplicatedEthernetRequest() throws Exception {
         final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mEm, times(1)).requestTetheredInterface(any(), any());
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verifyNoMoreInteractions(mEm);
         mTethering.stopTethering(TETHERING_ETHERNET);
@@ -2171,14 +2227,16 @@
         final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR);
 
         // Enable USB tethering and check that Tethering starts USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), firstResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+                firstResult);
         mLooper.dispatchAll();
         firstResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
         verifyNoMoreInteractions(mUsbManager);
 
         // Enable USB tethering again with the same request and expect no change to USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), secondResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+                secondResult);
         mLooper.dispatchAll();
         secondResult.assertHasResult();
         verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2187,7 +2245,8 @@
         // Enable USB tethering with a different request and expect that USB is stopped and
         // started.
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), thirdResult);
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+                  TEST_CALLER_PKG, thirdResult);
         mLooper.dispatchAll();
         thirdResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2216,7 +2275,8 @@
         final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
                 ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), null);
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+                  TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
         mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
@@ -2284,7 +2344,7 @@
         final TetheringRequestParcel wifiNotExemptRequest =
                 createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
                         CONNECTIVITY_SCOPE_GLOBAL);
-        mTethering.startTethering(wifiNotExemptRequest, null);
+        mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2298,7 +2358,7 @@
         final TetheringRequestParcel wifiExemptRequest =
                 createTetheringRequestParcel(TETHERING_WIFI, null, null, true,
                         CONNECTIVITY_SCOPE_GLOBAL);
-        mTethering.startTethering(wifiExemptRequest, null);
+        mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2311,14 +2371,14 @@
         // If one app enables tethering without provisioning check first, then another app enables
         // tethering of the same type but does not disable the provisioning check.
         setupForRequiredProvisioning();
-        mTethering.startTethering(wifiExemptRequest, null);
+        mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
         assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
         reset(mEntitleMgr);
         setupForRequiredProvisioning();
-        mTethering.startTethering(wifiNotExemptRequest, null);
+        mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2408,7 +2468,8 @@
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
         final ArgumentCaptor<TetheredInterfaceCallback> callbackCaptor =
                 ArgumentCaptor.forClass(TetheredInterfaceCallback.class);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET),
+                TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture());
         TetheredInterfaceCallback ethCallback = callbackCaptor.getValue();
@@ -2490,12 +2551,11 @@
         eventCallbacks = dhcpEventCbsCaptor.getValue();
         // Update lease for local only tethering.
         final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11");
-        final ArrayList<DhcpLeaseParcelable> p2pLeases = new ArrayList<>();
-        p2pLeases.add(createDhcpLeaseParcelable("clientId1", testMac1, "192.168.50.24", 24,
-                Long.MAX_VALUE, "test1"));
-        notifyDhcpLeasesChanged(p2pLeases, eventCallbacks);
-        final List<TetheredClient> clients = toTetheredClients(p2pLeases, TETHERING_WIFI_P2P);
-        callback.expectTetheredClientChanged(clients);
+        final DhcpLeaseParcelable p2pLease = createDhcpLeaseParcelable("clientId1", testMac1,
+                "192.168.50.24", 24, Long.MAX_VALUE, "test1");
+        final List<TetheredClient> p2pClients = notifyDhcpLeasesChanged(TETHERING_WIFI_P2P,
+                eventCallbacks, p2pLease);
+        callback.expectTetheredClientChanged(p2pClients);
         reset(mDhcpServer);
 
         // Run wifi tethering.
@@ -2505,25 +2565,20 @@
                 any(), dhcpEventCbsCaptor.capture());
         eventCallbacks = dhcpEventCbsCaptor.getValue();
         // Update mac address from softAp callback before getting dhcp lease.
-        final ArrayList<WifiClient> wifiClients = new ArrayList<>();
         final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22");
-        final WifiClient testClient = mock(WifiClient.class);
-        when(testClient.getMacAddress()).thenReturn(testMac2);
-        wifiClients.add(testClient);
-        mSoftApCallback.onConnectedClientsChanged(wifiClients);
-        final TetheredClient noAddrClient = new TetheredClient(testMac2,
-                Collections.emptyList() /* addresses */, TETHERING_WIFI);
-        clients.add(noAddrClient);
-        callback.expectTetheredClientChanged(clients);
+        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac2,
+                false /* isLocalOnly */);
+        final List<TetheredClient> p2pAndNoAddrClients = new ArrayList<>(p2pClients);
+        p2pAndNoAddrClients.add(noAddrClient);
+        callback.expectTetheredClientChanged(p2pAndNoAddrClients);
 
         // Update dhcp lease for wifi tethering.
-        clients.remove(noAddrClient);
-        final ArrayList<DhcpLeaseParcelable> wifiLeases = new ArrayList<>();
-        wifiLeases.add(createDhcpLeaseParcelable("clientId2", testMac2, "192.168.43.24", 24,
-                Long.MAX_VALUE, "test2"));
-        notifyDhcpLeasesChanged(wifiLeases, eventCallbacks);
-        clients.addAll(toTetheredClients(wifiLeases, TETHERING_WIFI));
-        callback.expectTetheredClientChanged(clients);
+        final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId2", testMac2,
+                "192.168.43.24", 24, Long.MAX_VALUE, "test2");
+        final List<TetheredClient> p2pAndWifiClients = new ArrayList<>(p2pClients);
+        p2pAndWifiClients.addAll(notifyDhcpLeasesChanged(TETHERING_WIFI,
+                eventCallbacks, wifiLease));
+        callback.expectTetheredClientChanged(p2pAndWifiClients);
 
         // Test onStarted callback that register second callback when tethering is running.
         TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
@@ -2531,18 +2586,74 @@
             mTethering.registerTetheringEventCallback(callback2);
             mLooper.dispatchAll();
         });
-        callback2.expectTetheredClientChanged(clients);
+        callback2.expectTetheredClientChanged(p2pAndWifiClients);
     }
 
-    private void notifyDhcpLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables,
-            IDhcpEventCallbacks callback) throws Exception {
-        callback.onLeasesChanged(leaseParcelables);
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testUpdateConnectedClientsForLocalOnlyHotspot() throws Exception {
+        TestTetheringEventCallback callback = new TestTetheringEventCallback();
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mTethering.registerTetheringEventCallback(callback);
+            mLooper.dispatchAll();
+        });
+        callback.expectTetheredClientChanged(Collections.emptyList());
+
+        // Run local only hotspot.
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+
+        final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
+                 ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+                any(), dhcpEventCbsCaptor.capture());
+        final IDhcpEventCallbacks eventCallbacks = dhcpEventCbsCaptor.getValue();
+        // Update mac address from softAp callback before getting dhcp lease.
+        final MacAddress testMac = MacAddress.fromString("22:22:22:22:22:22");
+        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac,
+                true /* isLocalOnly */);
+        final List<TetheredClient> noAddrLocalOnlyClients = new ArrayList<>();
+        noAddrLocalOnlyClients.add(noAddrClient);
+        callback.expectTetheredClientChanged(noAddrLocalOnlyClients);
+
+        // Update dhcp lease for local only hotspot.
+        final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId", testMac,
+                "192.168.43.24", 24, Long.MAX_VALUE, "test");
+        final List<TetheredClient> localOnlyClients = notifyDhcpLeasesChanged(TETHERING_WIFI,
+                eventCallbacks, wifiLease);
+        callback.expectTetheredClientChanged(localOnlyClients);
+
+        // Client disconnect from local only hotspot.
+        mLocalOnlyHotspotCallback.onConnectedClientsChanged(Collections.emptyList());
+        callback.expectTetheredClientChanged(Collections.emptyList());
+    }
+
+    private TetheredClient notifyConnectedWifiClientsChanged(final MacAddress mac,
+            boolean isLocalOnly) throws Exception {
+        final ArrayList<WifiClient> wifiClients = new ArrayList<>();
+        final WifiClient testClient = mock(WifiClient.class);
+        when(testClient.getMacAddress()).thenReturn(mac);
+        wifiClients.add(testClient);
+        if (isLocalOnly) {
+            mLocalOnlyHotspotCallback.onConnectedClientsChanged(wifiClients);
+        } else {
+            mSoftApCallback.onConnectedClientsChanged(wifiClients);
+        }
+        return new TetheredClient(mac, Collections.emptyList() /* addresses */, TETHERING_WIFI);
+    }
+
+    private List<TetheredClient> notifyDhcpLeasesChanged(int type, IDhcpEventCallbacks callback,
+            DhcpLeaseParcelable... leases) throws Exception {
+        final List<DhcpLeaseParcelable> dhcpLeases = Arrays.asList(leases);
+        callback.onLeasesChanged(dhcpLeases);
         mLooper.dispatchAll();
+
+        return toTetheredClients(dhcpLeases, type);
     }
 
     private List<TetheredClient> toTetheredClients(List<DhcpLeaseParcelable> leaseParcelables,
             int type) throws Exception {
-        final ArrayList<TetheredClient> leases = new ArrayList<>();
+        final ArrayList<TetheredClient> clients = new ArrayList<>();
         for (DhcpLeaseParcelable lease : leaseParcelables) {
             final LinkAddress address = new LinkAddress(
                     intToInet4AddressHTH(lease.netAddr), lease.prefixLength,
@@ -2552,13 +2663,13 @@
             final MacAddress macAddress = MacAddress.fromBytes(lease.hwAddr);
 
             final AddressInfo addressInfo = new TetheredClient.AddressInfo(address, lease.hostname);
-            leases.add(new TetheredClient(
+            clients.add(new TetheredClient(
                     macAddress,
                     Collections.singletonList(addressInfo),
                     type));
         }
 
-        return leases;
+        return clients;
     }
 
     private DhcpLeaseParcelable createDhcpLeaseParcelable(final String clientId,
@@ -2583,7 +2694,8 @@
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
         result.assertHasResult();
@@ -2618,7 +2730,8 @@
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
         result.assertHasResult();
@@ -2639,7 +2752,8 @@
         // already bound.
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
         final ResultListener secondResult = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), secondResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, secondResult);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, false /* bindToPanService */);
         secondResult.assertHasResult();
@@ -2660,7 +2774,8 @@
     public void testBluetoothServiceDisconnects() throws Exception {
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         ServiceListener panListener = verifySetBluetoothTethering(true /* enable */,
                 true /* bindToPanService */);
@@ -2811,18 +2926,26 @@
         runNcmTethering();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
+        verify(mTetheringMetrics).createBuilder(eq(TETHERING_NCM), anyString());
 
         // Change the USB tethering function to NCM. Because the USB tethering function was set to
         // RNDIS (the default), tethering is stopped.
         forceUsbTetheringUse(TETHER_USB_NCM_FUNCTION);
         verifyUsbTetheringStopDueToSettingChange(TEST_NCM_IFNAME);
+        verify(mTetheringMetrics).updateErrorCode(anyInt(), eq(TETHER_ERROR_NO_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_NCM));
 
         // If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
         // available.
         final ResultListener ncmResult = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), ncmResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+                ncmResult);
         mLooper.dispatchAll();
         ncmResult.assertHasResult();
+        verify(mTetheringMetrics, times(2)).createBuilder(eq(TETHERING_NCM), anyString());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_NCM),
+                eq(TETHER_ERROR_SERVICE_UNAVAIL));
+        verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_NCM));
 
         // Run TETHERING_USB with ncm configuration.
         runDualStackUsbTethering(TEST_NCM_IFNAME);
@@ -2836,53 +2959,81 @@
         runStopUSBTethering();
     }
 
+    public static ArraySet<Integer> getAllSupportedTetheringTypes() {
+        return new ArraySet<>(new Integer[] { TETHERING_USB, TETHERING_NCM, TETHERING_WIFI,
+                TETHERING_WIFI_P2P, TETHERING_BLUETOOTH, TETHERING_ETHERNET });
+    }
+
     @Test
     public void testTetheringSupported() throws Exception {
+        final ArraySet<Integer> expectedTypes = getAllSupportedTetheringTypes();
+        // Check tethering is supported after initialization.
         setTetheringSupported(true /* supported */);
-        updateConfigAndVerifySupported(true /* supported */);
+        TestTetheringEventCallback callback = new TestTetheringEventCallback();
+        mTethering.registerTetheringEventCallback(callback);
+        mLooper.dispatchAll();
+        updateConfigAndVerifySupported(callback, expectedTypes);
 
         // Could disable tethering supported by settings.
         Settings.Global.putInt(mContentResolver, Settings.Global.TETHER_SUPPORTED, 0);
-        updateConfigAndVerifySupported(false /* supported */);
+        updateConfigAndVerifySupported(callback, new ArraySet<>());
 
         // Could disable tethering supported by user restriction.
         setTetheringSupported(true /* supported */);
+        updateConfigAndVerifySupported(callback, expectedTypes);
         when(mUserManager.hasUserRestriction(
                 UserManager.DISALLOW_CONFIG_TETHERING)).thenReturn(true);
-        updateConfigAndVerifySupported(false /* supported */);
+        updateConfigAndVerifySupported(callback, new ArraySet<>());
 
         // Tethering is supported if it has any supported downstream.
         setTetheringSupported(true /* supported */);
+        updateConfigAndVerifySupported(callback, expectedTypes);
+        // Usb tethering is not supported:
+        expectedTypes.remove(TETHERING_USB);
         when(mResources.getStringArray(R.array.config_tether_usb_regexs))
                 .thenReturn(new String[0]);
-        updateConfigAndVerifySupported(true /* supported */);
+        updateConfigAndVerifySupported(callback, expectedTypes);
+        // Wifi tethering is not supported:
+        expectedTypes.remove(TETHERING_WIFI);
         when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
                 .thenReturn(new String[0]);
-        updateConfigAndVerifySupported(true /* supported */);
-
+        updateConfigAndVerifySupported(callback, expectedTypes);
+        // Bluetooth tethering is not supported:
+        expectedTypes.remove(TETHERING_BLUETOOTH);
+        when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
+                .thenReturn(new String[0]);
 
         if (isAtLeastT()) {
-            when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
-                    .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(true /* supported */);
+            updateConfigAndVerifySupported(callback, expectedTypes);
+
+            // P2p tethering is not supported:
+            expectedTypes.remove(TETHERING_WIFI_P2P);
             when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
                     .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(true /* supported */);
+            updateConfigAndVerifySupported(callback, expectedTypes);
+            // Ncm tethering is not supported:
+            expectedTypes.remove(TETHERING_NCM);
             when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
                     .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(true /* supported */);
+            updateConfigAndVerifySupported(callback, expectedTypes);
+            // Ethernet tethering (last supported type) is not supported:
+            expectedTypes.remove(TETHERING_ETHERNET);
             mForceEthernetServiceUnavailable = true;
-            updateConfigAndVerifySupported(false /* supported */);
+            updateConfigAndVerifySupported(callback, new ArraySet<>());
+
         } else {
-            when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
-                    .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(false /* supported */);
+            // If wifi, usb and bluetooth are all not supported, all the types are not supported.
+            expectedTypes.clear();
+            updateConfigAndVerifySupported(callback, expectedTypes);
         }
     }
 
-    private void updateConfigAndVerifySupported(boolean supported) {
+    private void updateConfigAndVerifySupported(final TestTetheringEventCallback callback,
+            final ArraySet<Integer> expectedTypes) {
         sendConfigurationChanged();
-        assertEquals(supported, mTethering.isTetheringSupported());
+
+        assertEquals(expectedTypes.size() > 0, mTethering.isTetheringSupported());
+        callback.expectSupportedTetheringTypes(expectedTypes);
     }
     // 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/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
new file mode 100644
index 0000000..c34cf5f
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.metrics;
+
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.stats.connectivity.DownstreamType;
+import android.stats.connectivity.ErrorCode;
+import android.stats.connectivity.UpstreamType;
+import android.stats.connectivity.UserType;
+import android.util.Pair;
+
+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.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class TetheringMetricsTest {
+    private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
+    private static final String SETTINGS_PKG = "com.android.settings";
+    private static final String SYSTEMUI_PKG = "com.android.systemui";
+    private static final String GMS_PKG = "com.google.android.gms";
+    private TetheringMetrics mTetheringMetrics;
+
+    private final NetworkTetheringReported.Builder mStatsBuilder =
+            NetworkTetheringReported.newBuilder();
+
+    private class MockTetheringMetrics extends TetheringMetrics {
+        @Override
+        public void write(final NetworkTetheringReported reported) { }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTetheringMetrics = spy(new MockTetheringMetrics());
+    }
+
+    private void runDownstreamTypesTest(final Pair<Integer, DownstreamType>... testPairs)
+            throws Exception {
+        for (Pair<Integer, DownstreamType> testPair : testPairs) {
+            final int type = testPair.first;
+            final DownstreamType expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
+            mTetheringMetrics.updateErrorCode(type, TETHER_ERROR_NO_ERROR);
+            mTetheringMetrics.sendReport(type);
+            NetworkTetheringReported expectedReport =
+                    mStatsBuilder.setDownstreamType(expectedResult)
+                    .setUserType(UserType.USER_UNKNOWN)
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(ErrorCode.EC_NO_ERROR)
+                    .build();
+            verify(mTetheringMetrics).write(expectedReport);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testDownstreamTypes() throws Exception {
+        runDownstreamTypesTest(new Pair<>(TETHERING_WIFI, DownstreamType.DS_TETHERING_WIFI),
+                new Pair<>(TETHERING_WIFI_P2P, DownstreamType.DS_TETHERING_WIFI_P2P),
+                new Pair<>(TETHERING_BLUETOOTH, DownstreamType.DS_TETHERING_BLUETOOTH),
+                new Pair<>(TETHERING_USB, DownstreamType.DS_TETHERING_USB),
+                new Pair<>(TETHERING_NCM, DownstreamType.DS_TETHERING_NCM),
+                new Pair<>(TETHERING_ETHERNET, DownstreamType.DS_TETHERING_ETHERNET));
+    }
+
+    private void runErrorCodesTest(final Pair<Integer, ErrorCode>... testPairs)
+            throws Exception {
+        for (Pair<Integer, ErrorCode> testPair : testPairs) {
+            final int errorCode = testPair.first;
+            final ErrorCode expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
+            mTetheringMetrics.updateErrorCode(TETHERING_WIFI, errorCode);
+            mTetheringMetrics.sendReport(TETHERING_WIFI);
+            NetworkTetheringReported expectedReport =
+                    mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
+                    .setUserType(UserType.USER_UNKNOWN)
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(expectedResult)
+                    .build();
+            verify(mTetheringMetrics).write(expectedReport);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testErrorCodes() throws Exception {
+        runErrorCodesTest(new Pair<>(TETHER_ERROR_NO_ERROR, ErrorCode.EC_NO_ERROR),
+                new Pair<>(TETHER_ERROR_UNKNOWN_IFACE, ErrorCode.EC_UNKNOWN_IFACE),
+                new Pair<>(TETHER_ERROR_SERVICE_UNAVAIL, ErrorCode.EC_SERVICE_UNAVAIL),
+                new Pair<>(TETHER_ERROR_UNSUPPORTED, ErrorCode.EC_UNSUPPORTED),
+                new Pair<>(TETHER_ERROR_UNAVAIL_IFACE, ErrorCode.EC_UNAVAIL_IFACE),
+                new Pair<>(TETHER_ERROR_INTERNAL_ERROR, ErrorCode.EC_INTERNAL_ERROR),
+                new Pair<>(TETHER_ERROR_TETHER_IFACE_ERROR, ErrorCode.EC_TETHER_IFACE_ERROR),
+                new Pair<>(TETHER_ERROR_UNTETHER_IFACE_ERROR, ErrorCode.EC_UNTETHER_IFACE_ERROR),
+                new Pair<>(TETHER_ERROR_ENABLE_FORWARDING_ERROR,
+                ErrorCode.EC_ENABLE_FORWARDING_ERROR),
+                new Pair<>(TETHER_ERROR_DISABLE_FORWARDING_ERROR,
+                ErrorCode.EC_DISABLE_FORWARDING_ERROR),
+                new Pair<>(TETHER_ERROR_IFACE_CFG_ERROR, ErrorCode.EC_IFACE_CFG_ERROR),
+                new Pair<>(TETHER_ERROR_PROVISIONING_FAILED, ErrorCode.EC_PROVISIONING_FAILED),
+                new Pair<>(TETHER_ERROR_DHCPSERVER_ERROR, ErrorCode.EC_DHCPSERVER_ERROR),
+                new Pair<>(TETHER_ERROR_ENTITLEMENT_UNKNOWN, ErrorCode.EC_ENTITLEMENT_UNKNOWN),
+                new Pair<>(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION,
+                ErrorCode.EC_NO_CHANGE_TETHERING_PERMISSION),
+                new Pair<>(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION,
+                ErrorCode.EC_NO_ACCESS_TETHERING_PERMISSION),
+                new Pair<>(TETHER_ERROR_UNKNOWN_TYPE, ErrorCode.EC_UNKNOWN_TYPE));
+    }
+
+    private void runUserTypesTest(final Pair<String, UserType>... testPairs)
+            throws Exception {
+        for (Pair<String, UserType> testPair : testPairs) {
+            final String callerPkg = testPair.first;
+            final UserType expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
+            mTetheringMetrics.updateErrorCode(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+            mTetheringMetrics.sendReport(TETHERING_WIFI);
+            NetworkTetheringReported expectedReport =
+                    mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
+                    .setUserType(expectedResult)
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(ErrorCode.EC_NO_ERROR)
+                    .build();
+            verify(mTetheringMetrics).write(expectedReport);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testUserTypes() throws Exception {
+        runUserTypesTest(new Pair<>(TEST_CALLER_PKG, UserType.USER_UNKNOWN),
+                new Pair<>(SETTINGS_PKG, UserType.USER_SETTINGS),
+                new Pair<>(SYSTEMUI_PKG, UserType.USER_SYSTEMUI),
+                new Pair<>(GMS_PKG, UserType.USER_GMS));
+    }
+}
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 6718402..23af3e3 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,8 +59,20 @@
 // bpf kernel programs
 //
 bpf {
+    name: "block.o",
+    srcs: ["block.c"],
+    btf: true,
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    sub_dir: "net_shared",
+}
+
+bpf {
     name: "dscp_policy.o",
     srcs: ["dscp_policy.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -86,8 +99,9 @@
 }
 
 bpf {
-    name: "clatd.o_mainline",
+    name: "clatd.o",
     srcs: ["clatd.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -99,8 +113,9 @@
 }
 
 bpf {
-    name: "netd.o_mainline",
+    name: "netd.o",
     srcs: ["netd.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -108,5 +123,5 @@
     include_dirs: [
         "frameworks/libs/net/common/netd/libnetdutils/include",
     ],
-    sub_dir: "net_shared",
+    sub_dir: "netd_shared",
 }
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
new file mode 100644
index 0000000..f2a3e62
--- /dev/null
+++ b/bpf_progs/block.c
@@ -0,0 +1,73 @@
+/*
+ * 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>
+
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
+#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/bpf_shared.h b/bpf_progs/bpf_shared.h
index 2ddc7b8..2afb789 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -98,29 +98,29 @@
 static const int CONFIGURATION_MAP_SIZE = 2;
 static const int UID_OWNER_MAP_SIZE = 2000;
 
-#define BPF_PATH "/sys/fs/bpf/"
+#define BPF_NETD_PATH "/sys/fs/bpf/netd_shared/"
 
-#define BPF_EGRESS_PROG_PATH BPF_PATH "prog_netd_cgroupskb_egress_stats"
-#define BPF_INGRESS_PROG_PATH BPF_PATH "prog_netd_cgroupskb_ingress_stats"
-#define XT_BPF_INGRESS_PROG_PATH BPF_PATH "prog_netd_skfilter_ingress_xtbpf"
-#define XT_BPF_EGRESS_PROG_PATH BPF_PATH "prog_netd_skfilter_egress_xtbpf"
-#define XT_BPF_ALLOWLIST_PROG_PATH BPF_PATH "prog_netd_skfilter_allowlist_xtbpf"
-#define XT_BPF_DENYLIST_PROG_PATH BPF_PATH "prog_netd_skfilter_denylist_xtbpf"
-#define CGROUP_SOCKET_PROG_PATH BPF_PATH "prog_netd_cgroupsock_inet_create"
+#define BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_egress_stats"
+#define BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_ingress_stats"
+#define XT_BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_ingress_xtbpf"
+#define XT_BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_egress_xtbpf"
+#define XT_BPF_ALLOWLIST_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf"
+#define XT_BPF_DENYLIST_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf"
+#define CGROUP_SOCKET_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
 
 #define TC_BPF_INGRESS_ACCOUNT_PROG_NAME "prog_netd_schedact_ingress_account"
-#define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
+#define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_NETD_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
 
-#define COOKIE_TAG_MAP_PATH BPF_PATH "map_netd_cookie_tag_map"
-#define UID_COUNTERSET_MAP_PATH BPF_PATH "map_netd_uid_counterset_map"
-#define APP_UID_STATS_MAP_PATH BPF_PATH "map_netd_app_uid_stats_map"
-#define STATS_MAP_A_PATH BPF_PATH "map_netd_stats_map_A"
-#define STATS_MAP_B_PATH BPF_PATH "map_netd_stats_map_B"
-#define IFACE_INDEX_NAME_MAP_PATH BPF_PATH "map_netd_iface_index_name_map"
-#define IFACE_STATS_MAP_PATH BPF_PATH "map_netd_iface_stats_map"
-#define CONFIGURATION_MAP_PATH BPF_PATH "map_netd_configuration_map"
-#define UID_OWNER_MAP_PATH BPF_PATH "map_netd_uid_owner_map"
-#define UID_PERMISSION_MAP_PATH BPF_PATH "map_netd_uid_permission_map"
+#define COOKIE_TAG_MAP_PATH BPF_NETD_PATH "map_netd_cookie_tag_map"
+#define UID_COUNTERSET_MAP_PATH BPF_NETD_PATH "map_netd_uid_counterset_map"
+#define APP_UID_STATS_MAP_PATH BPF_NETD_PATH "map_netd_app_uid_stats_map"
+#define STATS_MAP_A_PATH BPF_NETD_PATH "map_netd_stats_map_A"
+#define STATS_MAP_B_PATH BPF_NETD_PATH "map_netd_stats_map_B"
+#define IFACE_INDEX_NAME_MAP_PATH BPF_NETD_PATH "map_netd_iface_index_name_map"
+#define IFACE_STATS_MAP_PATH BPF_NETD_PATH "map_netd_iface_stats_map"
+#define CONFIGURATION_MAP_PATH BPF_NETD_PATH "map_netd_configuration_map"
+#define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
+#define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
 
 enum UidOwnerMatchType {
     NO_MATCH = 0,
@@ -132,6 +132,9 @@
     RESTRICTED_MATCH = (1 << 5),
     LOW_POWER_STANDBY_MATCH = (1 << 6),
     IIF_MATCH = (1 << 7),
+    LOCKDOWN_VPN_MATCH = (1 << 8),
+    OEM_DENY_1_MATCH = (1 << 9),
+    OEM_DENY_2_MATCH = (1 << 10),
 };
 
 enum BpfPermissionMatch {
@@ -146,9 +149,9 @@
     SELECT_MAP_B,
 };
 
-// TODO: change the configuration object from an 8-bit bitmask to an object with clearer
+// TODO: change the configuration object from a bitmask to an object with clearer
 // semantics, like a struct.
-typedef uint8_t BpfConfig;
+typedef uint32_t BpfConfig;
 static const BpfConfig DEFAULT_CONFIG = 0;
 
 typedef struct {
@@ -159,16 +162,20 @@
 } UidOwnerValue;
 STRUCT_SIZE(UidOwnerValue, 2 * 4);  // 8
 
+// Entry in the configuration map that stores which UID rules are enabled.
 #define UID_RULES_CONFIGURATION_KEY 1
+// Entry in the configuration map that stores which stats map is currently in use.
 #define CURRENT_STATS_MAP_CONFIGURATION_KEY 2
 
+#define BPF_CLATD_PATH "/sys/fs/bpf/net_shared/"
+
 #define CLAT_INGRESS6_PROG_RAWIP_NAME "prog_clatd_schedcls_ingress6_clat_rawip"
 #define CLAT_INGRESS6_PROG_ETHER_NAME "prog_clatd_schedcls_ingress6_clat_ether"
 
-#define CLAT_INGRESS6_PROG_RAWIP_PATH BPF_PATH CLAT_INGRESS6_PROG_RAWIP_NAME
-#define CLAT_INGRESS6_PROG_ETHER_PATH BPF_PATH CLAT_INGRESS6_PROG_ETHER_NAME
+#define CLAT_INGRESS6_PROG_RAWIP_PATH BPF_CLATD_PATH CLAT_INGRESS6_PROG_RAWIP_NAME
+#define CLAT_INGRESS6_PROG_ETHER_PATH BPF_CLATD_PATH CLAT_INGRESS6_PROG_ETHER_NAME
 
-#define CLAT_INGRESS6_MAP_PATH BPF_PATH "map_clatd_clat_ingress6_map"
+#define CLAT_INGRESS6_MAP_PATH BPF_CLATD_PATH "map_clatd_clat_ingress6_map"
 
 typedef struct {
     uint32_t iif;            // The input interface index
@@ -186,10 +193,10 @@
 #define CLAT_EGRESS4_PROG_RAWIP_NAME "prog_clatd_schedcls_egress4_clat_rawip"
 #define CLAT_EGRESS4_PROG_ETHER_NAME "prog_clatd_schedcls_egress4_clat_ether"
 
-#define CLAT_EGRESS4_PROG_RAWIP_PATH BPF_PATH CLAT_EGRESS4_PROG_RAWIP_NAME
-#define CLAT_EGRESS4_PROG_ETHER_PATH BPF_PATH CLAT_EGRESS4_PROG_ETHER_NAME
+#define CLAT_EGRESS4_PROG_RAWIP_PATH BPF_CLATD_PATH CLAT_EGRESS4_PROG_RAWIP_NAME
+#define CLAT_EGRESS4_PROG_ETHER_PATH BPF_CLATD_PATH CLAT_EGRESS4_PROG_ETHER_NAME
 
-#define CLAT_EGRESS4_MAP_PATH BPF_PATH "map_clatd_clat_egress4_map"
+#define CLAT_EGRESS4_MAP_PATH BPF_CLATD_PATH "map_clatd_clat_egress4_map"
 
 typedef struct {
     uint32_t iif;           // The input interface index
diff --git a/bpf_progs/bpf_tethering.h b/bpf_progs/bpf_tethering.h
index b0ec8f6..f9ef6ef 100644
--- a/bpf_progs/bpf_tethering.h
+++ b/bpf_progs/bpf_tethering.h
@@ -73,10 +73,6 @@
 #define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
 
 
-#define BPF_PATH_TETHER BPF_PATH "tethering/"
-
-#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map"
-
 typedef uint32_t TetherStatsKey;  // upstream ifindex
 
 typedef struct {
@@ -89,19 +85,9 @@
 } TetherStatsValue;
 STRUCT_SIZE(TetherStatsValue, 6 * 8);  // 48
 
-#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map"
-
 typedef uint32_t TetherLimitKey;    // upstream ifindex
 typedef uint64_t TetherLimitValue;  // in bytes
 
-#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip"
-#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether"
-
-#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME
-
-#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map"
-
 // For now tethering offload only needs to support downstreams that use 6-byte MAC addresses,
 // because all downstream types that are currently supported (WiFi, USB, Bluetooth and
 // Ethernet) have 6-byte MAC addresses.
@@ -121,8 +107,6 @@
 } Tether6Value;
 STRUCT_SIZE(Tether6Value, 4 + 14 + 2);  // 20
 
-#define TETHER_DOWNSTREAM64_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream64_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -146,14 +130,6 @@
 } TetherDownstream64Value;
 STRUCT_SIZE(TetherDownstream64Value, 4 + 14 + 2 + 4 + 4 + 2 + 2 + 8);  // 40
 
-#define TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream6_rawip"
-#define TETHER_UPSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream6_ether"
-
-#define TETHER_UPSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream6_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -162,23 +138,6 @@
 } TetherUpstream6Key;
 STRUCT_SIZE(TetherUpstream6Key, 12);
 
-#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream4_rawip"
-#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream4_ether"
-
-#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME
-
-#define TETHER_DOWNSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream4_map"
-
-
-#define TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream4_rawip"
-#define TETHER_UPSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream4_ether"
-
-#define TETHER_UPSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream4_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -202,16 +161,4 @@
 } Tether4Value;
 STRUCT_SIZE(Tether4Value, 4 + 14 + 2 + 16 + 16 + 2 + 2 + 8);  // 64
 
-#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_downstream_rawip"
-#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_downstream_ether"
-
-#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_upstream_rawip"
-#define TETHER_UPSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_upstream_ether"
-
-#define TETHER_UPSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_ETHER_NAME
-
 #undef STRUCT_SIZE
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index dc646c3..c5b8555 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -30,6 +30,9 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "bpf_shared.h"
@@ -37,21 +40,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 +79,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 +94,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 +151,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 +223,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..538a9e4 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>
@@ -26,250 +27,298 @@
 #include <netinet/udp.h>
 #include <string.h>
 
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #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..1637f7a
--- /dev/null
+++ b/bpf_progs/dscp_policy.h
@@ -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.
+ */
+
+#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.")
+
+#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])) == 0)
+
+// 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 fe9a871..9ae8ab2 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// The resulting .o needs to load on the Android T Beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include <bpf_helpers.h>
 #include <linux/bpf.h>
 #include <linux/if.h>
@@ -59,7 +62,7 @@
 DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
 DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE,
                    AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint8_t, CONFIGURATION_MAP_SIZE,
+DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint32_t, CONFIGURATION_MAP_SIZE,
                    AID_NET_BW_ACCT)
 DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE,
                    AID_NET_BW_ACCT)
@@ -194,7 +197,7 @@
     BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
 
     UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid);
-    uint8_t uidRules = uidEntry ? uidEntry->rule : 0;
+    uint32_t uidRules = uidEntry ? uidEntry->rule : 0;
     uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
 
     if (enabledRules) {
@@ -213,10 +216,23 @@
         if ((enabledRules & LOW_POWER_STANDBY_MATCH) && !(uidRules & LOW_POWER_STANDBY_MATCH)) {
             return BPF_DROP;
         }
+        if ((enabledRules & OEM_DENY_1_MATCH) && (uidRules & OEM_DENY_1_MATCH)) {
+            return BPF_DROP;
+        }
+        if ((enabledRules & OEM_DENY_2_MATCH) && (uidRules & OEM_DENY_2_MATCH)) {
+            return BPF_DROP;
+        }
     }
-    if (direction == BPF_INGRESS && (uidRules & IIF_MATCH)) {
-        // Drops packets not coming from lo nor the allowlisted interface
-        if (allowed_iif && skb->ifindex != 1 && skb->ifindex != allowed_iif) {
+    if (direction == BPF_INGRESS && skb->ifindex != 1) {
+        if (uidRules & IIF_MATCH) {
+            if (allowed_iif && skb->ifindex != allowed_iif) {
+                // Drops packets not coming from lo nor the allowed interface
+                // allowed interface=0 is a wildcard and does not drop packets
+                return BPF_DROP_UNLESS_DNS;
+            }
+        } else if (uidRules & LOCKDOWN_VPN_MATCH) {
+            // Drops packets not coming from lo and rule does not have IIF_MATCH but has
+            // LOCKDOWN_VPN_MATCH
             return BPF_DROP_UNLESS_DNS;
         }
     }
@@ -224,7 +240,7 @@
 }
 
 static __always_inline inline void update_stats_with_config(struct __sk_buff* skb, int direction,
-                                                            StatsKey* key, uint8_t selectedMap) {
+                                                            StatsKey* key, uint32_t selectedMap) {
     if (selectedMap == SELECT_MAP_A) {
         update_stats_map_A(skb, direction, key);
     } else if (selectedMap == SELECT_MAP_B) {
@@ -276,7 +292,7 @@
     if (counterSet) key.counterSet = (uint32_t)*counterSet;
 
     uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
-    uint8_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
+    uint32_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
 
     // Use asm("%0 &= 1" : "+r"(match)) before return match,
     // to help kernel's bpf verifier, so that it can be 100% certain
@@ -373,8 +389,7 @@
     return BPF_NOMATCH;
 }
 
-DEFINE_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
-                     KVER(4, 14, 0))
+DEFINE_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
 (struct bpf_sock* sk) {
     uint64_t gid_uid = bpf_get_current_uid_gid();
     /*
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 977e918..2ec0792 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -24,8 +24,8 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
-// The resulting .o needs to load on the Android S bpfloader v0.2
-#define BPFLOADER_MIN_VER 2u
+// The resulting .o needs to load on the Android S bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
@@ -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;
@@ -355,88 +355,10 @@
 
 DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
-        const bool downstream, const bool updatetime) {
-    // 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 IPv4 frame
-    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
-
-    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
-
-    // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
-    // 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);
-
-    void* data = (void*)(long)skb->data;
-    const void* data_end = (void*)(long)skb->data_end;
-    struct ethhdr* eth = is_ethernet ? data : NULL;  // used iff is_ethernet
-    struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
-
-    // Must have (ethernet and) ipv4 header
-    if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_PIPE;
-
-    // Ethertype - if present - must be IPv4
-    if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_PIPE;
-
-    // IP version must be 4
-    if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
-
-    // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
-    if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
-
-    // Calculate the IPv4 one's complement checksum of the IPv4 header.
-    __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
-        sum4 += ((__u16*)ip)[i];
-    }
-    // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
-    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse u32 into range 1 .. 0x1FFFE
-    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse any potential carry into u16
-    // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
-    if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
-
-    // Minimum IPv4 total length is the size of the header
-    if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
-
-    // We are incapable of dealing with IPv4 fragments
-    if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
-
-    // Cannot decrement during forward if already zero or would be zero,
-    // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
-    if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
-
-    // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
-    // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
-    // in such a situation we can only support TCP.  This also has the added nice benefit of
-    // using a separate error counter, and thus making it obvious which version of the program
-    // is loaded.
-    if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
-
-    // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
-    // but no need to check this if !updatetime due to check immediately above.
-    if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
-        TC_PUNT(NON_TCP_UDP);
-
-    // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
-    // out all the non-tcp logic.  Also note that at this point is_udp === !is_tcp.
-    const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
-
-    // This is a bit of a hack to make things easier on the bpf verifier.
-    // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
-    // what offsets into the packet are valid and can spuriously reject the program, this is
-    // because it fails to realize that is_tcp && !is_tcp is impossible)
-    //
-    // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
-    // always be in the first 4 bytes of the L4 header.  Additionally for UDP we'll need access
-    // to the checksum field which is in bytes 7 and 8.  While for TCP we'll need to read the
-    // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
-    // As such we *always* need access to at least 8 bytes.
-    if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
-
+static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
+        const int l2_header_size, void* data, const void* data_end,
+        struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
+        const bool downstream, const bool updatetime, const bool is_tcp) {
     struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
     struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
 
@@ -625,6 +547,102 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
+static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
+        const bool downstream, const bool updatetime) {
+    // 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 IPv4 frame
+    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
+
+    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+    // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
+    // 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_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
+
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+    struct ethhdr* eth = is_ethernet ? data : NULL;  // used iff is_ethernet
+    struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
+
+    // Must have (ethernet and) ipv4 header
+    if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_PIPE;
+
+    // Ethertype - if present - must be IPv4
+    if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_PIPE;
+
+    // IP version must be 4
+    if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
+
+    // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+    if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
+
+    // Calculate the IPv4 one's complement checksum of the IPv4 header.
+    __wsum sum4 = 0;
+    for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+        sum4 += ((__u16*)ip)[i];
+    }
+    // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse u32 into range 1 .. 0x1FFFE
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse any potential carry into u16
+    // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
+    if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
+
+    // Minimum IPv4 total length is the size of the header
+    if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
+
+    // We are incapable of dealing with IPv4 fragments
+    if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
+
+    // Cannot decrement during forward if already zero or would be zero,
+    // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
+    if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
+
+    // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
+    // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
+    // in such a situation we can only support TCP.  This also has the added nice benefit of
+    // using a separate error counter, and thus making it obvious which version of the program
+    // is loaded.
+    if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+
+    // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
+    // but no need to check this if !updatetime due to check immediately above.
+    if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+        TC_PUNT(NON_TCP_UDP);
+
+    // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
+    // out all the non-tcp logic.  Also note that at this point is_udp === !is_tcp.
+    const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+
+    // This is a bit of a hack to make things easier on the bpf verifier.
+    // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
+    // what offsets into the packet are valid and can spuriously reject the program, this is
+    // because it fails to realize that is_tcp && !is_tcp is impossible)
+    //
+    // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
+    // always be in the first 4 bytes of the L4 header.  Additionally for UDP we'll need access
+    // to the checksum field which is in bytes 7 and 8.  While for TCP we'll need to read the
+    // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
+    // As such we *always* need access to at least 8 bytes.
+    if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
+
+    // We're forcing the compiler to emit two copies of the following code, optimized
+    // separately for is_tcp being true or false.  This simplifies the resulting bpf
+    // byte code sufficiently that the 4.14 bpf verifier is able to keep track of things.
+    // Without this (updatetime == true) case would fail to bpf verify on 4.14 even
+    // if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
+    if (is_tcp) {
+      return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
+                                is_ethernet, downstream, updatetime, /* is_tcp */ true);
+    } else {
+      return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
+                                is_ethernet, downstream, updatetime, /* is_tcp */ false);
+    }
+}
+
 // Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index c9c73f1..f2fcc8c 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -18,8 +18,8 @@
 #include <linux/in.h>
 #include <linux/ip.h>
 
-// The resulting .o needs to load on the Android S bpfloader v0.2
-#define BPFLOADER_MIN_VER 2u
+// The resulting .o needs to load on the Android S bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index f46d887..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,
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 4fefa0a..eb77288 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -3,7 +3,7 @@
 
   public final class NetworkStats implements java.lang.AutoCloseable {
     method public void close();
-    method public boolean getNextBucket(android.app.usage.NetworkStats.Bucket);
+    method public boolean getNextBucket(@Nullable android.app.usage.NetworkStats.Bucket);
     method public boolean hasNextBucket();
   }
 
@@ -40,21 +40,21 @@
   }
 
   public class NetworkStatsManager {
-    method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, String, long, long, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, String, long, long, int, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, String, long, long, int, int, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats querySummary(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method public void registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback);
-    method public void registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
-    method public void unregisterUsageCallback(android.app.usage.NetworkStatsManager.UsageCallback);
+    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, String);
+    method public abstract void onThresholdReached(int, @Nullable String);
   }
 
 }
@@ -173,12 +173,12 @@
     method public static void incrementOperationCount(int, int);
     method public static void setThreadStatsTag(int);
     method public static void setThreadStatsUid(int);
-    method public static void tagDatagramSocket(java.net.DatagramSocket) throws java.net.SocketException;
-    method public static void tagFileDescriptor(java.io.FileDescriptor) throws java.io.IOException;
-    method public static void tagSocket(java.net.Socket) throws java.net.SocketException;
-    method public static void untagDatagramSocket(java.net.DatagramSocket) throws java.net.SocketException;
-    method public static void untagFileDescriptor(java.io.FileDescriptor) throws java.io.IOException;
-    method public static void untagSocket(java.net.Socket) throws java.net.SocketException;
+    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
   }
 
@@ -188,10 +188,12 @@
 
   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 53e1beb..2996a3e 100644
--- a/framework-t/api/lint-baseline.txt
+++ b/framework-t/api/lint-baseline.txt
@@ -41,86 +41,18 @@
     android.net.IpSecTransform.Builder does not declare a `build()` method, but builder classes are expected to
 
 
-MissingNullability: android.app.usage.NetworkStats#getNextBucket(android.app.usage.NetworkStats.Bucket) parameter #0:
-    Missing nullability on parameter `bucketOut` in method `getNextBucket`
 MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
     Missing nullability on method `queryDetails` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetails`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int):
-    Missing nullability on method `queryDetailsForUid` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUid`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int):
-    Missing nullability on method `queryDetailsForUidTag` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUidTag`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int):
-    Missing nullability on method `queryDetailsForUidTagState` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUidTagState`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
     Missing nullability on method `querySummary` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummary`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
     Missing nullability on method `querySummaryForDevice` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummaryForDevice`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
     Missing nullability on method `querySummaryForUser` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummaryForUser`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback) parameter #3:
-    Missing nullability on parameter `callback` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler) parameter #3:
-    Missing nullability on parameter `callback` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#unregisterUsageCallback(android.app.usage.NetworkStatsManager.UsageCallback) parameter #0:
-    Missing nullability on parameter `callback` in method `unregisterUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager.UsageCallback#onThresholdReached(int, String) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `onThresholdReached`
 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
-MissingNullability: android.net.TrafficStats#tagDatagramSocket(java.net.DatagramSocket) parameter #0:
-    Missing nullability on parameter `socket` in method `tagDatagramSocket`
-MissingNullability: android.net.TrafficStats#tagFileDescriptor(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `tagFileDescriptor`
-MissingNullability: android.net.TrafficStats#tagSocket(java.net.Socket) parameter #0:
-    Missing nullability on parameter `socket` in method `tagSocket`
-MissingNullability: android.net.TrafficStats#untagDatagramSocket(java.net.DatagramSocket) parameter #0:
-    Missing nullability on parameter `socket` in method `untagDatagramSocket`
-MissingNullability: android.net.TrafficStats#untagFileDescriptor(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `untagFileDescriptor`
-MissingNullability: android.net.TrafficStats#untagSocket(java.net.Socket) parameter #0:
-    Missing nullability on parameter `socket` in method `untagSocket`
-MissingNullability: com.android.internal.util.FileRotator#FileRotator(java.io.File, String, long, long) parameter #0:
-    Missing nullability on parameter `basePath` in method `FileRotator`
-MissingNullability: com.android.internal.util.FileRotator#FileRotator(java.io.File, String, long, long) parameter #1:
-    Missing nullability on parameter `prefix` in method `FileRotator`
-MissingNullability: com.android.internal.util.FileRotator#dumpAll(java.io.OutputStream) parameter #0:
-    Missing nullability on parameter `os` in method `dumpAll`
-MissingNullability: com.android.internal.util.FileRotator#readMatching(com.android.internal.util.FileRotator.Reader, long, long) parameter #0:
-    Missing nullability on parameter `reader` in method `readMatching`
-MissingNullability: com.android.internal.util.FileRotator#rewriteActive(com.android.internal.util.FileRotator.Rewriter, long) parameter #0:
-    Missing nullability on parameter `rewriter` in method `rewriteActive`
-MissingNullability: com.android.internal.util.FileRotator#rewriteAll(com.android.internal.util.FileRotator.Rewriter) parameter #0:
-    Missing nullability on parameter `rewriter` in method `rewriteAll`
-MissingNullability: com.android.internal.util.FileRotator.Reader#read(java.io.InputStream) parameter #0:
-    Missing nullability on parameter `in` in method `read`
-MissingNullability: com.android.internal.util.FileRotator.Writer#write(java.io.OutputStream) parameter #0:
-    Missing nullability on parameter `out` in method `write`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#kernelToTag(String) parameter #0:
-    Missing nullability on parameter `string` in method `kernelToTag`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#tag(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `tag`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#untag(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `untag`
 
 
 RethrowRemoteException: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 1bdd388..c1f7b39 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -4,6 +4,8 @@
   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);
@@ -32,9 +34,15 @@
   }
 
   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
@@ -176,6 +184,7 @@
   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 {
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 0149115..6460fed 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -2,10 +2,8 @@
 package android.app.usage {
 
   public class NetworkStatsManager {
-    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 @NonNull @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 @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
+    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);
   }
 
 }
@@ -13,10 +11,10 @@
 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 connectNetwork(@NonNull String, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.BiConsumer<android.net.Network,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 disconnectNetwork(@NonNull String, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.BiConsumer<android.net.Network,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 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 java.util.function.BiConsumer<android.net.Network,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 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 {
@@ -113,7 +111,6 @@
   public class TrafficStats {
     method public static void setThreadStatsTagApp();
     method public static void setThreadStatsTagBackup();
-    method public static void setThreadStatsTagDownload();
     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
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..b8070f0
--- /dev/null
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -0,0 +1,705 @@
+/*
+ * 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.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Build;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+
+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.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 ArrayMap<InterfaceStateListener, IEthernetServiceListener>
+            mIfaceServiceListeners = new ArrayMap<>();
+    @GuardedBy("mListenerLock")
+    private final ArrayMap<IntConsumer, IEthernetServiceListener> mStateServiceListeners =
+            new ArrayMap<>();
+    final Object mListenerLock = new Object();
+
+    /**
+     * 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;
+
+    /**
+     * 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");
+        }
+
+        final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() {
+            @Override
+            public void onEthernetStateChanged(int state) {}
+
+            @Override
+            public void onInterfaceStateChanged(String iface, int state, int role,
+                    IpConfiguration configuration) {
+                executor.execute(() ->
+                        listener.onInterfaceStateChanged(iface, state, role, configuration));
+            }
+        };
+        synchronized (mListenerLock) {
+            addServiceListener(serviceListener);
+            mIfaceServiceListeners.put(listener, serviceListener);
+        }
+    }
+
+    @GuardedBy("mListenerLock")
+    private void addServiceListener(@NonNull final IEthernetServiceListener listener) {
+        try {
+            mService.addListener(listener);
+        } 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) {
+            maybeRemoveServiceListener(mIfaceServiceListeners.remove(listener));
+        }
+    }
+
+    @GuardedBy("mListenerLock")
+    private void maybeRemoveServiceListener(@Nullable final IEthernetServiceListener listener) {
+        if (listener == null) return;
+
+        try {
+            mService.removeListener(listener);
+        } 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})
+    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.enableInterface(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})
+    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.disableInterface(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);
+        final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() {
+            @Override
+            public void onEthernetStateChanged(int state) {
+                executor.execute(() -> listener.accept(state));
+            }
+
+            @Override
+            public void onInterfaceStateChanged(String iface, int state, int role,
+                    IpConfiguration configuration) {}
+        };
+        synchronized (mListenerLock) {
+            addServiceListener(serviceListener);
+            mStateServiceListeners.put(listener, serviceListener);
+        }
+    }
+
+    /**
+     * 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) {
+            maybeRemoveServiceListener(mStateServiceListeners.remove(listener));
+        }
+    }
+
+    /**
+     * 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..c1efc29
--- /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 enableInterface(String iface, in INetworkInterfaceOutcomeReceiver listener);
+    void disableInterface(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..9cceac2
--- /dev/null
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -0,0 +1,1064 @@
+/*
+ * 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 be a functional {@link Network} with valid {@link LinkProperties},
+         *     and 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..6a1d2dd
--- /dev/null
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -0,0 +1,991 @@
+/*
+ * 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;
+            }
+        }
+    }
+
+    /**
+     * Remove histories which contains or is before the cutoff timestamp.
+     * @hide
+     */
+    public void removeHistoryBefore(long cutoffMillis) {
+        final ArrayList<Key> knownKeys = new ArrayList<>();
+        knownKeys.addAll(mStats.keySet());
+
+        for (Key key : knownKeys) {
+            final NetworkStatsHistory history = mStats.get(key);
+            if (history.getStart() > cutoffMillis) continue;
+
+            history.removeBucketsStartingBefore(cutoffMillis);
+            if (history.size() == 0) {
+                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}.
+         *            If history already exists for this key, then the passed-in history is appended
+         *            to the previously-passed in history. The caller must ensure that the history
+         *            passed-in timestamps are greater than all previously-passed-in timestamps.
+         * @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 existing = mEntries.get(key);
+
+            final int size = historyEntries.size() + ((existing != null) ? existing.size() : 0);
+            final NetworkStatsHistory.Builder historyBuilder =
+                    new NetworkStatsHistory.Builder(mBucketDurationMillis, size);
+
+            // TODO: this simply appends the entries to any entries that were already present in
+            // the builder, which requires the caller to pass in entries in order. We might be
+            // able to do better with something like recordHistory.
+            if (existing != null) {
+                for (Entry entry : existing.getEntries()) {
+                    historyBuilder.addEntry(entry);
+                }
+            }
+
+            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..0ff9d96
--- /dev/null
+++ b/framework-t/src/android/net/NetworkStatsHistory.java
@@ -0,0 +1,1214 @@
+/*
+ * 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.Nullable;
+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 that start older than requested cutoff.
+     *
+     * This method will remove any bucket that contains any data older than the requested
+     * cutoff, even if that same bucket includes some data from after the cutoff.
+     *
+     * @hide
+     */
+    public void removeBucketsStartingBefore(final long cutoff) {
+        // TODO: Consider use getIndexBefore.
+        int i;
+        for (i = 0; i < bucketCount; i++) {
+            final long curStart = bucketStart[i];
+
+            // This bucket starts after or at the cutoff, so it should be kept.
+            if (curStart >= 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();
+    }
+
+    /**
+     * Same as "equals", but not actually called equals as this would affect public API behavior.
+     * @hide
+     */
+    @Nullable
+    public boolean isSameAs(NetworkStatsHistory other) {
+        return bucketCount == other.bucketCount
+                && Arrays.equals(bucketStart, other.bucketStart)
+                // Don't check activeTime since it can change on import due to the importer using
+                // recordHistory. It's also not exposed by the APIs or present in dumpsys or
+                // toString().
+                && Arrays.equals(rxBytes, other.rxBytes)
+                && Arrays.equals(rxPackets, other.rxPackets)
+                && Arrays.equals(txBytes, other.txBytes)
+                && Arrays.equals(txPackets, other.txPackets)
+                && Arrays.equals(operations, other.operations)
+                && totalBytes == other.totalBytes;
+    }
+
+    @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);
+        }
+
+        private void addToElement(List<Long> list, int pos, long value) {
+            list.set(pos, list.get(pos) + value);
+        }
+
+        /**
+         * Add an {@link Entry} into the {@link NetworkStatsHistory} instance.
+         *
+         * @param entry The target {@link Entry} object. The entry timestamp must be greater than
+         *              that of any previously-added entry.
+         * @return The builder object.
+         */
+        @NonNull
+        public Builder addEntry(@NonNull Entry entry) {
+            final int lastBucket = mBucketStart.size() - 1;
+            final long lastBucketStart = (lastBucket != -1) ? mBucketStart.get(lastBucket) : 0;
+
+            // If last bucket has the same timestamp, modify it instead of adding another bucket.
+            // This allows callers to pass in the same bucket twice (e.g., to accumulate
+            // data over time), but still requires that entries must be sorted.
+            // The importer will do this in case a rotated file has the same timestamp as
+            // the previous file.
+            if (lastBucket != -1 && entry.bucketStart == lastBucketStart) {
+                addToElement(mActiveTime, lastBucket, entry.activeTime);
+                addToElement(mRxBytes, lastBucket, entry.rxBytes);
+                addToElement(mRxPackets, lastBucket, entry.rxPackets);
+                addToElement(mTxBytes, lastBucket, entry.txBytes);
+                addToElement(mTxPackets, lastBucket, entry.txPackets);
+                addToElement(mOperations, lastBucket, entry.operations);
+                return this;
+            }
+
+            // Inserting in the middle is prohibited for performance reasons.
+            if (entry.bucketStart <= lastBucketStart) {
+                throw new IllegalArgumentException("new bucket start " + entry.bucketStart
+                        + " must be greater than last bucket start " + lastBucketStart);
+            }
+
+            // Common case: add entries at the end of the list.
+            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..fad63e5
--- /dev/null
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -0,0 +1,1094 @@
+/*
+ * 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
+     */
+    // TODO: Deprecate this since NSD service is never disabled.
+    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 MDNS_SERVICE_EVENT                      = 20;
+
+    /** @hide */
+    public static final int REGISTER_CLIENT                         = 21;
+    /** @hide */
+    public static final int UNREGISTER_CLIENT                       = 22;
+
+    /** 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(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, mBaseExecutor);
+                mPerNetworkListeners.put(network, wrappedListener);
+                // Run discovery callbacks inline on the service handler thread, which is the
+                // same thread used by this NetworkCallback, but DelegatingDiscoveryListener will
+                // use the base executor to run the wrapped callbacks.
+                discoverServices(mServiceType, mProtocolType, network, Runnable::run,
+                        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(() -> mBaseExecutor.execute(() ->
+                    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) {
+                    mBaseExecutor.execute(() -> 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);
+            }
+        }
+
+        /**
+         * A listener wrapping calls to an app-provided listener, while keeping track of found
+         * services, so they can all be reported lost when the underlying network is lost.
+         *
+         * This should be registered to run on the service handler.
+         */
+        private class DelegatingDiscoveryListener implements DiscoveryListener {
+            private final Network mNetwork;
+            private final DiscoveryListener mWrapped;
+            private final Executor mWrappedExecutor;
+            private final ArraySet<TrackedNsdInfo> mFoundInfo = new ArraySet<>();
+
+            private DelegatingDiscoveryListener(Network network, DiscoveryListener listener,
+                    Executor executor) {
+                mNetwork = network;
+                mWrapped = listener;
+                mWrappedExecutor = executor;
+            }
+
+            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);
+                    mWrappedExecutor.execute(() -> 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.
+                    mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType));
+                }
+            }
+
+            @Override
+            public void onDiscoveryStopped(String serviceType) {
+                mPerNetworkListeners.remove(mNetwork);
+                if (mStopRequested && mPerNetworkListeners.size() == 0) {
+                    mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType));
+                }
+            }
+
+            @Override
+            public void onServiceFound(NsdServiceInfo serviceInfo) {
+                mFoundInfo.add(new TrackedNsdInfo(serviceInfo));
+                mWrappedExecutor.execute(() -> mWrapped.onServiceFound(serviceInfo));
+            }
+
+            @Override
+            public void onServiceLost(NsdServiceInfo serviceInfo) {
+                mFoundInfo.remove(new TrackedNsdInfo(serviceInfo));
+                mWrappedExecutor.execute(() -> 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) {
+            // Do not use message in the executor lambdas, as it will be recycled once this method
+            // returns. Keep references to its content instead.
+            final int what = message.what;
+            final int errorCode = message.arg1;
+            final int key = message.arg2;
+            final Object obj = message.obj;
+            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 " + key);
+                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) obj);
+                    executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s));
+                    break;
+                case DISCOVER_SERVICES_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed(
+                            getNsdServiceInfoType(ns), errorCode));
+                    break;
+                case SERVICE_FOUND:
+                    executor.execute(() -> ((DiscoveryListener) listener).onServiceFound(
+                            (NsdServiceInfo) obj));
+                    break;
+                case SERVICE_LOST:
+                    executor.execute(() -> ((DiscoveryListener) listener).onServiceLost(
+                            (NsdServiceInfo) 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), errorCode));
+                    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, errorCode));
+                    break;
+                case REGISTER_SERVICE_SUCCEEDED:
+                    executor.execute(() -> ((RegistrationListener) listener).onServiceRegistered(
+                            (NsdServiceInfo) obj));
+                    break;
+                case UNREGISTER_SERVICE_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((RegistrationListener) listener).onUnregistrationFailed(
+                            ns, errorCode));
+                    break;
+                case UNREGISTER_SERVICE_SUCCEEDED:
+                    // TODO: do not unregister listener until service is unregistered, or provide
+                    // alternative way for unregistering ?
+                    removeListener(key);
+                    executor.execute(() -> ((RegistrationListener) listener).onServiceUnregistered(
+                            ns));
+                    break;
+                case RESOLVE_SERVICE_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((ResolveListener) listener).onResolveFailed(
+                            ns, errorCode));
+                    break;
+                case RESOLVE_SERVICE_SUCCEEDED:
+                    removeListener(key);
+                    executor.execute(() -> ((ResolveListener) listener).onServiceResolved(
+                            (NsdServiceInfo) 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..200c808
--- /dev/null
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -0,0 +1,442 @@
+/*
+ * 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;
+
+    private int mInterfaceIndex;
+
+    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 set if this {@link NsdServiceInfo} was obtained from
+     * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}, unless the service
+     * was found on a network interface that does not have a {@link Network} (such as a tethering
+     * downstream, where services are advertised from devices connected to this device via
+     * tethering).
+     */
+    @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;
+    }
+
+    /**
+     * Get the index of the network interface where the service was found.
+     *
+     * This is only set when the service was found on an interface that does not have a usable
+     * Network, in which case {@link #getNetwork()} returns null.
+     * @return The interface index as per {@link java.net.NetworkInterface#getIndex}, or 0 if unset.
+     * @hide
+     */
+    public int getInterfaceIndex() {
+        return mInterfaceIndex;
+    }
+
+    /**
+     * Set the index of the network interface where the service was found.
+     * @hide
+     */
+    public void setInterfaceIndex(int interfaceIndex) {
+        mInterfaceIndex = interfaceIndex;
+    }
+
+    @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);
+        dest.writeInt(mInterfaceIndex);
+    }
+
+    /** 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);
+                info.mInterfaceIndex = in.readInt();
+                return info;
+            }
+
+            public NsdServiceInfo[] newArray(int size) {
+                return new NsdServiceInfo[size];
+            }
+        };
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index 53f9217..d7de439 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -64,6 +64,7 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
+        ":framework-connectivity-javastream-protos",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -85,12 +86,13 @@
         "net-utils-device-common",
     ],
     static_libs: [
-        "framework-connectivity-protos",
+        "mdns_aidl_interface-lateststable-java",
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
     ],
     libs: [
+        "app-compat-annotations",
         "framework-connectivity-t.stubs.module_lib",
         "unsupportedappusage",
     ],
@@ -151,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",
@@ -182,22 +189,36 @@
     ],
 }
 
-// TODO: reduce size of this library; consider using
-// proto nano for example
-java_library {
+filegroup {
     name: "framework-connectivity-protos",
-    sdk_version: "module_current",
-    min_sdk_version: "30",
-    proto: {
-        type: "lite",
-    },
     srcs: [
-        "proto/**/*.*",
+        "proto/**/*.proto",
     ],
-   static_libs: ["libprotobuf-java-lite"],
-    apex_available: [
-        "com.android.tethering",
+    visibility: ["//frameworks/base"],
+}
+
+gensrcs {
+    name: "framework-connectivity-javastream-protos",
+    depfile: true,
+
+    tools: [
+        "aprotoc",
+        "protoc-gen-javastream",
+        "soong_zip",
     ],
-    lint: { strict_updatability_linting: true },
-    visibility: ["//visibility:private"],
+
+    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 e4e2151..ddac19d 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -30,10 +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 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);
     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";
@@ -54,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
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 53d485d..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 {
@@ -236,7 +230,6 @@
   public abstract class NetworkAgent {
     ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, int, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
     ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkScore, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
-    method public void destroyAndAwaitReplacement(@IntRange(from=0, to=0x1388) int);
     method @Nullable public android.net.Network getNetwork();
     method public void markConnected();
     method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData);
@@ -271,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
   }
@@ -340,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();
   }
 
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a798f6e..fdc7bf7 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -982,6 +982,30 @@
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
 
+    /**
+     * Firewall chain used for lockdown VPN.
+     * Denylist of apps that cannot receive incoming packets except on loopback because they are
+     * subject to an always-on VPN which is not currently connected.
+     *
+     * @see #BLOCKED_REASON_LOCKDOWN_VPN
+     * @hide
+     */
+    public static final int FIREWALL_CHAIN_LOCKDOWN_VPN = 6;
+
+    /**
+     * Firewall chain used for OEM-specific application restrictions.
+     * Denylist of apps that will not have network access due to OEM-specific restrictions.
+     * @hide
+     */
+    public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7;
+
+    /**
+     * Firewall chain used for OEM-specific application restrictions.
+     * Denylist of apps that will not have network access due to OEM-specific restrictions.
+     * @hide
+     */
+    public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = {
@@ -989,12 +1013,49 @@
         FIREWALL_CHAIN_STANDBY,
         FIREWALL_CHAIN_POWERSAVE,
         FIREWALL_CHAIN_RESTRICTED,
-        FIREWALL_CHAIN_LOW_POWER_STANDBY
+        FIREWALL_CHAIN_LOW_POWER_STANDBY,
+        FIREWALL_CHAIN_LOCKDOWN_VPN,
+        FIREWALL_CHAIN_OEM_DENY_1,
+        FIREWALL_CHAIN_OEM_DENY_2
     })
     public @interface FirewallChain {}
     // 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
@@ -2555,9 +2616,24 @@
      * {@hide}
      */
     public ConnectivityManager(Context context, IConnectivityManager service) {
+        this(context, service, true /* newStatic */);
+    }
+
+    private ConnectivityManager(Context context, IConnectivityManager service, boolean newStatic) {
         mContext = Objects.requireNonNull(context, "missing context");
         mService = Objects.requireNonNull(service, "missing IConnectivityManager");
-        sInstance = this;
+        // sInstance is accessed without a lock, so it may actually be reassigned several times with
+        // different ConnectivityManager, but that's still OK considering its usage.
+        if (sInstance == null && newStatic) {
+            final Context appContext = mContext.getApplicationContext();
+            // Don't create static ConnectivityManager instance again to prevent infinite loop.
+            // If the application context is null, we're either in the system process or
+            // it's the application context very early in app initialization. In both these
+            // cases, the passed-in Context will not be freed, so it's safe to pass it to the
+            // service. http://b/27532714 .
+            sInstance = new ConnectivityManager(appContext != null ? appContext : context, service,
+                    false /* newStatic */);
+        }
     }
 
     /** {@hide} */
@@ -5802,8 +5878,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)
@@ -5812,10 +5889,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();
         }
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 0988bf3..bc73769 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -240,7 +240,7 @@
 
     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);
 
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index 2b22a5c..b375b7b 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -47,5 +47,5 @@
     void sendAddDscpPolicy(in DscpPolicy policy);
     void sendRemoveDscpPolicy(int policyId);
     void sendRemoveAllDscpPolicies();
-    void sendDestroyAndAwaitReplacement(int timeoutMillis);
+    void sendUnregisterAfterReplacement(int timeoutMillis);
 }
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
index 847f14e..27d13c1 100644
--- a/framework/src/android/net/ITestNetworkManager.aidl
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -29,7 +29,8 @@
  */
 interface ITestNetworkManager
 {
-    TestNetworkInterface createInterface(boolean isTun, boolean bringUp, in LinkAddress[] addrs);
+    TestNetworkInterface createInterface(boolean isTun, boolean bringUp, in LinkAddress[] addrs,
+            in @nullable String iface);
 
     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..a8f707e 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_V2)
+    @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());
         }
@@ -1332,6 +1366,21 @@
     }
 
     /**
+     * Returns true if this link has a throw route.
+     *
+     * @return {@code true} if there is an exclude route, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasExcludeRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.getType() == RouteInfo.RTN_THROW) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Compares this {@code LinkProperties} interface name against the target
      *
      * @param target LinkProperties to compare.
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index fdc9081..2c50c73 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;
@@ -435,12 +434,54 @@
     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_DESTROY_AND_AWAIT_REPLACEMENT = BASE + 29;
+    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,
@@ -984,9 +1025,9 @@
      * @param timeoutMillis the timeout after which this network will be unregistered even if
      *                      {@link #unregister} was not called.
      */
-    public void destroyAndAwaitReplacement(
+    public void unregisterAfterReplacement(
             @IntRange(from = 0, to = MAX_TEARDOWN_DELAY_MS) int timeoutMillis) {
-        queueOrSendMessage(reg -> reg.sendDestroyAndAwaitReplacement(timeoutMillis));
+        queueOrSendMessage(reg -> reg.sendUnregisterAfterReplacement(timeoutMillis));
     }
 
     /**
@@ -1035,11 +1076,12 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public final void sendNetworkInfo(NetworkInfo networkInfo) {
-        queueOrSendNetworkInfo(new NetworkInfo(networkInfo));
+        queueOrSendNetworkInfo(networkInfo);
     }
 
     private void queueOrSendNetworkInfo(NetworkInfo networkInfo) {
-        queueOrSendMessage(reg -> reg.sendNetworkInfo(networkInfo));
+        final NetworkInfo ni = new NetworkInfo(networkInfo);
+        queueOrSendMessage(reg -> reg.sendNetworkInfo(ni));
     }
 
     /**
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index b28c006..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;
 
 /**
@@ -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 f7f2f57..97b1f32 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -863,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
@@ -2650,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
@@ -3058,9 +3061,20 @@
         /**
          * 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/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index fb271e3..fdcab02 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -120,8 +120,8 @@
     public String toString() {
         return "ProfileNetworkPreference{"
                 + "mPreference=" + getPreference()
-                + "mIncludedUids=" + mIncludedUids.toString()
-                + "mExcludedUids=" + mExcludedUids.toString()
+                + "mIncludedUids=" + Arrays.toString(mIncludedUids)
+                + "mExcludedUids=" + Arrays.toString(mExcludedUids)
                 + "mPreferenceEnterpriseId=" + mPreferenceEnterpriseId
                 + '}';
     }
diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java
index ed6eb15..b80cff4 100644
--- a/framework/src/android/net/QosCallbackException.java
+++ b/framework/src/android/net/QosCallbackException.java
@@ -46,8 +46,10 @@
             EX_TYPE_FILTER_NONE,
             EX_TYPE_FILTER_NETWORK_RELEASED,
             EX_TYPE_FILTER_SOCKET_NOT_BOUND,
+            EX_TYPE_FILTER_SOCKET_NOT_CONNECTED,
             EX_TYPE_FILTER_NOT_SUPPORTED,
             EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED,
+            EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ExceptionType {}
@@ -65,10 +67,16 @@
     public static final int EX_TYPE_FILTER_SOCKET_NOT_BOUND = 2;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 3;
+    public static final int EX_TYPE_FILTER_SOCKET_NOT_CONNECTED = 3;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 4;
+    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 4;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 5;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED = 6;
 
     /**
      * Creates exception based off of a type and message.  Not all types of exceptions accept a
@@ -83,12 +91,17 @@
                 return new QosCallbackException(new NetworkReleasedException());
             case EX_TYPE_FILTER_SOCKET_NOT_BOUND:
                 return new QosCallbackException(new SocketNotBoundException());
+            case EX_TYPE_FILTER_SOCKET_NOT_CONNECTED:
+                return new QosCallbackException(new SocketNotConnectedException());
             case EX_TYPE_FILTER_NOT_SUPPORTED:
                 return new QosCallbackException(new UnsupportedOperationException(
                         "This device does not support the specified filter"));
             case EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED:
                 return new QosCallbackException(
                         new SocketLocalAddressChangedException());
+            case EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED:
+                return new QosCallbackException(
+                        new SocketRemoteAddressChangedException());
             default:
                 Log.wtf(TAG, "create: No case setup for exception type: '" + type + "'");
                 return new QosCallbackException(
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
index 5c1c3cc..b432644 100644
--- a/framework/src/android/net/QosFilter.java
+++ b/framework/src/android/net/QosFilter.java
@@ -90,5 +90,15 @@
      */
     public abstract boolean matchesRemoteAddress(@NonNull InetAddress address,
             int startPort, int endPort);
+
+    /**
+     * Determines whether or not the parameter will be matched with this filter.
+     *
+     * @param protocol the protocol such as TCP or UDP included in IP packet filter set of a QoS
+     *                 flow assigned on {@link Network}.
+     * @return whether the parameters match the socket type of the filter
+     * @hide
+     */
+    public abstract boolean matchesProtocol(int protocol);
 }
 
diff --git a/framework/src/android/net/QosFilterParcelable.java b/framework/src/android/net/QosFilterParcelable.java
index da3b2cf..6e71fa3 100644
--- a/framework/src/android/net/QosFilterParcelable.java
+++ b/framework/src/android/net/QosFilterParcelable.java
@@ -104,7 +104,7 @@
         if (mQosFilter instanceof QosSocketFilter) {
             dest.writeInt(QOS_SOCKET_FILTER);
             final QosSocketFilter qosSocketFilter = (QosSocketFilter) mQosFilter;
-            qosSocketFilter.getQosSocketInfo().writeToParcel(dest, 0);
+            qosSocketFilter.getQosSocketInfo().writeToParcelWithoutFd(dest, 0);
             return;
         }
         dest.writeInt(NO_FILTER_PRESENT);
diff --git a/framework/src/android/net/QosSocketFilter.java b/framework/src/android/net/QosSocketFilter.java
index 69da7f4..5ceeb67 100644
--- a/framework/src/android/net/QosSocketFilter.java
+++ b/framework/src/android/net/QosSocketFilter.java
@@ -18,6 +18,13 @@
 
 import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
 import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -74,19 +81,34 @@
      * 2. In the scenario that the socket is now bound to a different local address, which can
      *    happen in the case of UDP, then
      *    {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED} is returned.
+     * 3. In the scenario that the UDP socket changed remote address, then
+     *    {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED} is returned.
+     *
      * @return validation error code
      */
     @Override
     public int validate() {
-        final InetSocketAddress sa = getAddressFromFileDescriptor();
-        if (sa == null) {
-            return QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+        final InetSocketAddress sa = getLocalAddressFromFileDescriptor();
+
+        if (sa == null || (sa.getAddress().isAnyLocalAddress() && sa.getPort() == 0)) {
+            return EX_TYPE_FILTER_SOCKET_NOT_BOUND;
         }
 
         if (!sa.equals(mQosSocketInfo.getLocalSocketAddress())) {
             return EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
         }
 
+        if (mQosSocketInfo.getRemoteSocketAddress() != null) {
+            final InetSocketAddress da = getRemoteAddressFromFileDescriptor();
+            if (da == null) {
+                return EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+            }
+
+            if (!da.equals(mQosSocketInfo.getRemoteSocketAddress())) {
+                return EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+            }
+        }
+
         return EX_TYPE_FILTER_NONE;
     }
 
@@ -98,17 +120,14 @@
      * @return the local address
      */
     @Nullable
-    private InetSocketAddress getAddressFromFileDescriptor() {
+    private InetSocketAddress getLocalAddressFromFileDescriptor() {
         final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor();
-        if (parcelFileDescriptor == null) return null;
-
         final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
-        if (fd == null) return null;
 
         final SocketAddress address;
         try {
             address = Os.getsockname(fd);
-        } catch (final ErrnoException e) {
+        } catch (ErrnoException e) {
             Log.e(TAG, "getAddressFromFileDescriptor: getLocalAddress exception", e);
             return null;
         }
@@ -119,6 +138,31 @@
     }
 
     /**
+     * The remote address of the socket's connected.
+     *
+     * <p>Note: If the socket is no longer connected, null is returned.
+     *
+     * @return the remote address
+     */
+    @Nullable
+    private InetSocketAddress getRemoteAddressFromFileDescriptor() {
+        final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor();
+        final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
+
+        final SocketAddress address;
+        try {
+            address = Os.getpeername(fd);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "getAddressFromFileDescriptor: getRemoteAddress exception", e);
+            return null;
+        }
+        if (address instanceof InetSocketAddress) {
+            return (InetSocketAddress) address;
+        }
+        return null;
+    }
+
+    /**
      * The network used with this filter.
      *
      * @return the registered {@link Network}
@@ -156,6 +200,18 @@
     }
 
     /**
+     * @inheritDoc
+     */
+    @Override
+    public boolean matchesProtocol(final int protocol) {
+        if ((mQosSocketInfo.getSocketType() == SOCK_STREAM && protocol == IPPROTO_TCP)
+                || (mQosSocketInfo.getSocketType() == SOCK_DGRAM && protocol == IPPROTO_UDP)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Called from {@link QosSocketFilter#matchesLocalAddress(InetAddress, int, int)}
      * and {@link QosSocketFilter#matchesRemoteAddress(InetAddress, int, int)} with the
      * filterSocketAddress coming from {@link QosSocketInfo#getLocalSocketAddress()}.
@@ -174,6 +230,7 @@
             final int startPort, final int endPort) {
         return startPort <= filterSocketAddress.getPort()
                 && endPort >= filterSocketAddress.getPort()
-                && filterSocketAddress.getAddress().equals(address);
+                && (address.isAnyLocalAddress()
+                        || filterSocketAddress.getAddress().equals(address));
     }
 }
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
index a45d507..49ac22b 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();
@@ -123,23 +165,28 @@
     /* Parcelable methods */
     private QosSocketInfo(final Parcel in) {
         mNetwork = Objects.requireNonNull(Network.CREATOR.createFromParcel(in));
-        mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+        final boolean withFd = in.readBoolean();
+        if (withFd) {
+            mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+        } else {
+            mParcelFileDescriptor = null;
+        }
 
-        final int localAddressLength = in.readInt();
-        mLocalSocketAddress = readSocketAddress(in, localAddressLength);
+        mLocalSocketAddress = readSocketAddress(in);
+        mRemoteSocketAddress = readSocketAddress(in);
 
-        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) {
-        final byte[] address = new byte[addressLength];
-        in.readByteArray(address);
+    private InetSocketAddress readSocketAddress(final Parcel in) {
+        final byte[] addrBytes = in.createByteArray();
+        if (addrBytes == null) {
+            return null;
+        }
         final int port = in.readInt();
 
         try {
-            return new InetSocketAddress(InetAddress.getByAddress(address), port);
+            return new InetSocketAddress(InetAddress.getByAddress(addrBytes), port);
         } catch (final UnknownHostException e) {
             /* This can never happen. UnknownHostException will never be thrown
                since the address provided is numeric and non-null. */
@@ -154,22 +201,38 @@
 
     @Override
     public void writeToParcel(@NonNull final Parcel dest, final int flags) {
-        mNetwork.writeToParcel(dest, 0);
-        mParcelFileDescriptor.writeToParcel(dest, 0);
+        writeToParcelInternal(dest, flags, /*includeFd=*/ true);
+    }
 
-        final byte[] localAddress = mLocalSocketAddress.getAddress().getAddress();
-        dest.writeInt(localAddress.length);
-        dest.writeByteArray(localAddress);
+    /**
+     * Used when sending QosSocketInfo to telephony, which does not need access to the socket FD.
+     * @hide
+     */
+    public void writeToParcelWithoutFd(@NonNull final Parcel dest, final int flags) {
+        writeToParcelInternal(dest, flags, /*includeFd=*/ false);
+    }
+
+    private void writeToParcelInternal(
+            @NonNull final Parcel dest, final int flags, boolean includeFd) {
+        mNetwork.writeToParcel(dest, 0);
+
+        if (includeFd) {
+            dest.writeBoolean(true);
+            mParcelFileDescriptor.writeToParcel(dest, 0);
+        } else {
+            dest.writeBoolean(false);
+        }
+
+        dest.writeByteArray(mLocalSocketAddress.getAddress().getAddress());
         dest.writeInt(mLocalSocketAddress.getPort());
 
         if (mRemoteSocketAddress == null) {
-            dest.writeInt(0);
+            dest.writeByteArray(null);
         } else {
-            final byte[] remoteAddress = mRemoteSocketAddress.getAddress().getAddress();
-            dest.writeInt(remoteAddress.length);
-            dest.writeByteArray(remoteAddress);
+            dest.writeByteArray(mRemoteSocketAddress.getAddress().getAddress());
             dest.writeInt(mRemoteSocketAddress.getPort());
         }
+        dest.writeInt(mSocketType);
     }
 
     @NonNull
diff --git a/framework/src/android/net/SocketNotConnectedException.java b/framework/src/android/net/SocketNotConnectedException.java
new file mode 100644
index 0000000..fa2a615
--- /dev/null
+++ b/framework/src/android/net/SocketNotConnectedException.java
@@ -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;
+
+/**
+ * Thrown when a previously bound socket becomes unbound.
+ *
+ * @hide
+ */
+public class SocketNotConnectedException extends Exception {
+    /** @hide */
+    public SocketNotConnectedException() {
+        super("The socket is not connected");
+    }
+}
diff --git a/framework/src/android/net/SocketRemoteAddressChangedException.java b/framework/src/android/net/SocketRemoteAddressChangedException.java
new file mode 100644
index 0000000..ecaeebc
--- /dev/null
+++ b/framework/src/android/net/SocketRemoteAddressChangedException.java
@@ -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;
+
+/**
+ * Thrown when the local address of the socket has changed.
+ *
+ * @hide
+ */
+public class SocketRemoteAddressChangedException extends Exception {
+    /** @hide */
+    public SocketRemoteAddressChangedException() {
+        super("The remote address of the socket changed");
+    }
+}
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 280e497..4e78823 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -45,6 +45,12 @@
      */
     public static final String TEST_TAP_PREFIX = "testtap";
 
+    /**
+     * Prefix for clat interfaces.
+     * @hide
+     */
+    public static final String CLAT_INTERFACE_PREFIX = "v4-";
+
     @NonNull private static final String TAG = TestNetworkManager.class.getSimpleName();
 
     @NonNull private final ITestNetworkManager mService;
@@ -160,7 +166,8 @@
     public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
         try {
             final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
-            return mService.createInterface(TUN, BRING_UP, linkAddrs.toArray(arr));
+            return mService.createInterface(TUN, BRING_UP, linkAddrs.toArray(arr),
+                    null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -178,7 +185,7 @@
     @NonNull
     public TestNetworkInterface createTapInterface() {
         try {
-            return mService.createInterface(TAP, BRING_UP, NO_ADDRS);
+            return mService.createInterface(TAP, BRING_UP, NO_ADDRS, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -197,7 +204,29 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp) {
         try {
-            return mService.createInterface(TAP, bringUp, NO_ADDRS);
+            return mService.createInterface(TAP, bringUp, NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface with a given interface name for testing purposes
+     *
+     * @param bringUp whether to bring up the interface before returning it.
+     * @param iface interface name to be assigned, so far only interface name which starts with
+     *              "v4-testtap" or "v4-testtun" is allowed to be created. If it's null, then use
+     *              the default name(e.g. testtap or testtun).
+     *
+     * @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, @NonNull String iface) {
+        try {
+            return mService.createInterface(TAP, bringUp, NO_ADDRS, iface);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/netd/Android.bp b/netd/Android.bp
index 5ac02d3..c731b8b 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -55,7 +55,8 @@
 cc_test {
     name: "netd_updatable_unit_test",
     defaults: ["netd_defaults"],
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
     require_root: true,  // required by setrlimitForTest()
     header_libs: [
         "bpf_connectivity_headers",
@@ -72,6 +73,7 @@
         "liblog",
         "libnetdutils",
     ],
+    compile_multilib: "both",
     multilib: {
         lib32: {
             suffix: "32",
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index f3dfb57..42d0de5 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -73,16 +73,7 @@
     }
     RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_EGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_EGRESS));
     RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_INGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_INGRESS));
-
-    // For the devices that support cgroup socket filter, the socket filter
-    // should be loaded successfully by bpfloader. So we attach the filter to
-    // cgroup if the program is pinned properly.
-    // TODO: delete the if statement once all devices should support cgroup
-    // socket filter (ie. the minimum kernel version required is 4.14).
-    if (!access(CGROUP_SOCKET_PROG_PATH, F_OK)) {
-        RETURN_IF_NOT_OK(
-                attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
-    }
+    RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
     return netdutils::status::ok;
 }
 
@@ -113,6 +104,7 @@
     RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A,
                                                   BPF_ANY));
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+    ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
 }
@@ -242,7 +234,7 @@
     if (sock_cookie == NONEXISTENT_COOKIE) return -errno;
     base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
     if (!res.ok()) {
-        ALOGE("Failed to untag socket: %s\n", strerror(res.error().code()));
+        ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
         return -res.error().code();
     }
     return 0;
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index 2ede1c1..05b9ebc 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -62,7 +62,7 @@
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
     BpfMap<StatsKey, StatsValue> mStatsMapA;
     BpfMap<StatsKey, StatsValue> mStatsMapB;
-    BpfMap<uint32_t, uint8_t> mConfigurationMap;
+    BpfMap<uint32_t, uint32_t> mConfigurationMap;
     BpfMap<uint32_t, uint8_t> mUidPermissionMap;
 
     std::mutex mMutex;
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index cd6b565..1bd222d 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -21,6 +21,7 @@
 
 #include <gtest/gtest.h>
 
+#define TEST_BPF_MAP
 #include "BpfHandler.h"
 
 using namespace android::bpf;  // NOLINT(google-build-using-namespace): exempted
@@ -48,46 +49,38 @@
     BpfHandler mBh;
     BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
     BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
-    BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+    BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
 
     void SetUp() {
         std::lock_guard guard(mBh.mMutex);
         ASSERT_EQ(0, setrlimitForTest());
 
-        mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
-                                          TEST_MAP_SIZE, 0));
+        mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeCookieTagMap);
 
-        mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
-                                       TEST_MAP_SIZE, 0));
+        mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeStatsMapA);
 
-        mFakeConfigurationMap.reset(
-                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_HASH, 1);
         ASSERT_VALID(mFakeConfigurationMap);
 
-        mFakeUidPermissionMap.reset(
-                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
         ASSERT_VALID(mFakeUidPermissionMap);
 
-        mBh.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+        mBh.mCookieTagMap = mFakeCookieTagMap;
         ASSERT_VALID(mBh.mCookieTagMap);
-        mBh.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+        mBh.mStatsMapA = mFakeStatsMapA;
         ASSERT_VALID(mBh.mStatsMapA);
-        mBh.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+        mBh.mConfigurationMap = mFakeConfigurationMap;
         ASSERT_VALID(mBh.mConfigurationMap);
         // Always write to stats map A by default.
         ASSERT_RESULT_OK(mBh.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
                                                           SELECT_MAP_A, BPF_ANY));
-        mBh.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
+        mBh.mUidPermissionMap = mFakeUidPermissionMap;
         ASSERT_VALID(mBh.mUidPermissionMap);
     }
 
-    int dupFd(const android::base::unique_fd& mapFd) {
-        return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
-    }
-
     int setUpSocketAndTag(int protocol, uint64_t* cookie, uint32_t tag, uid_t uid,
                           uid_t realUid) {
         int sock = socket(protocol, SOCK_STREAM | SOCK_CLOEXEC, 0);
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 8851554..1b9f2ec 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -19,6 +19,20 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// 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
@@ -29,9 +43,7 @@
     // TODO(b/210962470): Bump this to at least S, and then T.
     min_sdk_version: "30",
     srcs: [
-        "src/**/*.java",
-        ":ethernet-service-updatable-sources",
-        ":services.connectivity-tiramisu-updatable-sources",
+        ":service-connectivity-tiramisu-sources",
     ],
     libs: [
         "framework-annotations-lib",
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 bf56fd5..5b3d314 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -48,7 +48,8 @@
 
 cc_test {
     name: "libnetworkstats_test",
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
     require_root: true,  // required by setrlimitForTest()
     header_libs: ["bpf_connectivity_headers"],
     srcs: [
@@ -68,4 +69,13 @@
         "libbase",
         "liblog",
     ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
 }
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 4d605ce..c67821f 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -193,7 +193,7 @@
         return ret;
     }
 
-    BpfMapRO<uint32_t, uint8_t> configurationMap(CONFIGURATION_MAP_PATH);
+    BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
     if (!configurationMap.isValid()) {
         int ret = -errno;
         ALOGE("get configuration map fd failed: %s", strerror(errno));
@@ -205,6 +205,10 @@
               configuration.error().message().c_str());
         return -configuration.error().code();
     }
+    if (configuration.value() != SELECT_MAP_A && configuration.value() != SELECT_MAP_B) {
+        ALOGE("%s unknown configuration value: %d", __func__, configuration.value());
+        return -EINVAL;
+    }
     const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
     BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
     if (!statsMap.isValid()) {
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index fa86f39..626c2eb 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -21,6 +21,7 @@
 
 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;
@@ -31,6 +32,7 @@
  */
 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;
@@ -44,6 +46,7 @@
         mEthernetServiceImpl = createEthernetService(context);
         mConnectivity = new ConnectivityService(context);
         mIpSecService = createIpSecService(context);
+        mConnectivityNative = createConnectivityNativeService(context);
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
     }
@@ -65,6 +68,12 @@
             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);
@@ -98,15 +107,24 @@
         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;
-        try {
-            return NsdService.create(context);
-        } catch (InterruptedException e) {
-            Log.d(TAG, "Unable to get NSD service", e);
-            return null;
-        }
+
+        return NsdService.create(context);
     }
 
     /** Return Nearby service instance or null if current SDK is lower than T */
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..16b9f1e
--- /dev/null
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -0,0 +1,1883 @@
+/*
+ * 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 (lp == null) {
+            throw new IllegalArgumentException(
+                    "LinkProperties is null. The underlyingNetwork may not be functional");
+        }
+
+        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/NsdService.java b/service-t/src/com/android/server/NsdService.java
new file mode 100644
index 0000000..7115720
--- /dev/null
+++ b/service-t/src/com/android/server/NsdService.java
@@ -0,0 +1,1060 @@
+/*
+ * 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.INetd;
+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 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(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 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.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;
+                        }
+                        setServiceNetworkForCallback(servInfo, info.netId, info.interfaceIdx);
+                        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 set to null (netId 0) 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
+                        setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
+                        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 = unescape(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;
+                        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 (netId != NETID_UNSET && serviceHost != null) {
+                            clientInfo.mResolvedService.setHost(serviceHost);
+                            setServiceNetworkForCallback(clientInfo.mResolvedService,
+                                    netId, info.interfaceIdx);
+                            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;
+            }
+       }
+    }
+
+    private static void setServiceNetworkForCallback(NsdServiceInfo info, int netId, int ifaceIdx) {
+        switch (netId) {
+            case NETID_UNSET:
+                info.setNetwork(null);
+                break;
+            case INetd.LOCAL_NET_ID:
+                // Special case for LOCAL_NET_ID: Networks on netId 99 are not generally
+                // visible / usable for apps, so do not return it. Store the interface
+                // index instead, so at least if the client tries to resolve the service
+                // with that NsdServiceInfo, it will be done on the same interface.
+                // If they recreate the NsdServiceInfo themselves, resolution would be
+                // done on all interfaces as before T, which should also work.
+                info.setNetwork(null);
+                info.setInterfaceIndex(ifaceIdx);
+                break;
+            default:
+                info.setNetwork(new Network(netId));
+        }
+    }
+
+    // The full service name is escaped from standard DNS rules on mdnsresponder, making it suitable
+    // for passing to standard system DNS APIs such as res_query() . Thus, make the service name
+    // unescape for getting right service address. See "Notes on DNS Name Escaping" on
+    // external/mdnsresponder/mDNSShared/dns_sd.h for more details.
+    private String unescape(String s) {
+        StringBuilder sb = new StringBuilder(s.length());
+        for (int i = 0; i < s.length(); ++i) {
+            char c = s.charAt(i);
+            if (c == '\\') {
+                if (++i >= s.length()) {
+                    Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+                    break;
+                }
+                c = s.charAt(i);
+                if (c != '.' && c != '\\') {
+                    if (i + 2 >= s.length()) {
+                        Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+                        break;
+                    }
+                    c = (char) ((c - '0') * 100 + (s.charAt(i + 1) - '0') * 10
+                            + (s.charAt(i + 2) - '0'));
+                    i += 2;
+                }
+            }
+            sb.append(c);
+        }
+        return sb.toString();
+    }
+
+    @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();
+        final int registerInterface = getNetworkInterfaceIndex(service);
+        if (service.getNetwork() != null && registerInterface == IFACE_IDX_ANY) {
+            Log.e(TAG, "Interface to register service on not found");
+            return false;
+        }
+        return mMDnsManager.registerService(regId, name, type, port, textRecord, registerInterface);
+    }
+
+    private boolean unregisterService(int regId) {
+        return mMDnsManager.stopOperation(regId);
+    }
+
+    private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) {
+        final String type = serviceInfo.getServiceType();
+        final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
+        if (serviceInfo.getNetwork() != 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 int resolveInterface = getNetworkInterfaceIndex(service);
+        if (service.getNetwork() != 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(NsdServiceInfo serviceInfo) {
+        final Network network = serviceInfo.getNetwork();
+        if (network == null) {
+            // Fallback to getInterfaceIndex if present (typically if the NsdServiceInfo was
+            // provided by NsdService from discovery results, and the service was found on an
+            // interface that has no app-usable Network).
+            if (serviceInfo.getInterfaceIndex() != 0) {
+                return serviceInfo.getInterfaceIndex();
+            }
+            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..6006539
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -0,0 +1,147 @@
+/*
+ * 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 com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+
+import android.annotation.Nullable;
+import android.content.ApexEnvironment;
+import android.net.IpConfiguration;
+import android.os.Environment;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.net.IpConfigStore;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * This class provides an API to store and manage Ethernet network configuration.
+ */
+public class EthernetConfigStore {
+    private static final String TAG = EthernetConfigStore.class.getSimpleName();
+    private static final String CONFIG_FILE = "ipconfig.txt";
+    private static final String FILE_PATH = "/misc/ethernet/";
+    private static final String LEGACY_IP_CONFIG_FILE_PATH = Environment.getDataDirectory()
+            + FILE_PATH;
+    private static final String APEX_IP_CONFIG_FILE_PATH = ApexEnvironment.getApexEnvironment(
+            TETHERING_MODULE_NAME).getDeviceProtectedDataDir() + FILE_PATH;
+
+    private IpConfigStore mStore = new IpConfigStore();
+    private final ArrayMap<String, IpConfiguration> mIpConfigurations;
+    private IpConfiguration mIpConfigurationForDefaultInterface;
+    private final Object mSync = new Object();
+
+    public EthernetConfigStore() {
+        mIpConfigurations = new ArrayMap<>(0);
+    }
+
+    private static boolean doesConfigFileExist(final String filepath) {
+        return new File(filepath).exists();
+    }
+
+    private void writeLegacyIpConfigToApexPath(final String newFilePath, final String oldFilePath,
+            final String filename) {
+        final File directory = new File(newFilePath);
+        if (!directory.exists()) {
+            directory.mkdirs();
+        }
+
+        // Write the legacy IP config to the apex file path.
+        FileOutputStream fos = null;
+        final AtomicFile dst = new AtomicFile(new File(newFilePath + filename));
+        final AtomicFile src = new AtomicFile(new File(oldFilePath + filename));
+        try {
+            final byte[] raw = src.readFully();
+            if (raw.length > 0) {
+                fos = dst.startWrite();
+                fos.write(raw);
+                fos.flush();
+                dst.finishWrite(fos);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Fail to sync the legacy IP config to the apex file path.");
+            dst.failWrite(fos);
+        }
+    }
+
+    public void read() {
+        read(APEX_IP_CONFIG_FILE_PATH, LEGACY_IP_CONFIG_FILE_PATH, CONFIG_FILE);
+    }
+
+    @VisibleForTesting
+    void read(final String newFilePath, final String oldFilePath, final String filename) {
+        synchronized (mSync) {
+            // Attempt to read the IP configuration from apex file path first.
+            if (doesConfigFileExist(newFilePath + filename)) {
+                loadConfigFileLocked(newFilePath + filename);
+                return;
+            }
+
+            // If the config file doesn't exist in the apex file path, attempt to read it from
+            // the legacy file path, if config file exists, write the legacy IP configuration to
+            // apex config file path, this should just happen on the first boot. New or updated
+            // config entries are only written to the apex config file later.
+            if (!doesConfigFileExist(oldFilePath + filename)) return;
+            loadConfigFileLocked(oldFilePath + filename);
+            writeLegacyIpConfigToApexPath(newFilePath, oldFilePath, filename);
+        }
+    }
+
+    private void loadConfigFileLocked(final String filepath) {
+        final ArrayMap<String, IpConfiguration> configs =
+                IpConfigStore.readIpConfigurations(filepath);
+        mIpConfigurations.putAll(configs);
+    }
+
+    public void write(String iface, IpConfiguration config) {
+        write(iface, config, APEX_IP_CONFIG_FILE_PATH + CONFIG_FILE);
+    }
+
+    @VisibleForTesting
+    void write(String iface, IpConfiguration config, String filepath) {
+        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(filepath, mIpConfigurations);
+            }
+        }
+    }
+
+    public ArrayMap<String, IpConfiguration> getIpConfigurations() {
+        synchronized (mSync) {
+            return new ArrayMap<>(mIpConfigurations);
+        }
+    }
+
+    @Nullable
+    public IpConfiguration getIpConfigurationForDefaultInterface() {
+        return null;
+    }
+}
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..79802fb
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -0,0 +1,735 @@
+/*
+ * 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.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.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+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.ArraySet;
+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.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * {@link NetworkProvider} that manages NetworkOffers for Ethernet networks.
+ */
+public class EthernetNetworkFactory {
+    private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
+    final static boolean DBG = true;
+
+    private static final String NETWORK_TYPE = "Ethernet";
+
+    private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces =
+            new ConcurrentHashMap<>();
+    private final Handler mHandler;
+    private final Context mContext;
+    private final NetworkProvider mProvider;
+    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);
+        }
+
+        public String getTcpBufferSizesFromResource(Context context) {
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getString(R.string.config_ethernet_tcp_buffers);
+        }
+    }
+
+    public static class ConfigurationException extends AndroidRuntimeException {
+        public ConfigurationException(String msg) {
+            super(msg);
+        }
+    }
+
+    public EthernetNetworkFactory(Handler handler, Context context) {
+        this(handler, context, new NetworkProvider(context, handler.getLooper(), TAG),
+            new Dependencies());
+    }
+
+    @VisibleForTesting
+    EthernetNetworkFactory(Handler handler, Context context, NetworkProvider provider,
+            Dependencies deps) {
+        mHandler = handler;
+        mContext = context;
+        mProvider = provider;
+        mDeps = deps;
+    }
+
+    /**
+     * Registers the network provider with the system.
+     */
+    public void register() {
+        mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
+    }
+
+    /**
+     * 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, mProvider, mDeps);
+        mTrackingInterfaces.put(ifaceName, iface);
+    }
+
+    @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);
+    }
+
+    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 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.destroy();
+        }
+    }
+
+    /** 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 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 NetworkProvider mNetworkProvider;
+        private final Dependencies mDeps;
+        private final NetworkProvider.NetworkOfferCallback mNetworkOfferCallback;
+
+        private static String sTcpBufferSizes = null;  // Lazy initialized.
+
+        private boolean mLinkUp;
+        private int mLegacyType;
+        private LinkProperties mLinkProperties = new LinkProperties();
+        private Set<NetworkRequest> mRequests = new ArraySet<>();
+
+        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);
+        }
+
+        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();
+            }
+        }
+
+        private class EthernetNetworkOfferCallback implements NetworkProvider.NetworkOfferCallback {
+            @Override
+            public void onNetworkNeeded(@NonNull NetworkRequest request) {
+                if (DBG) {
+                    Log.d(TAG, String.format("%s: onNetworkNeeded for request: %s", name, request));
+                }
+                // When the network offer is first registered, onNetworkNeeded is called with all
+                // existing requests.
+                // ConnectivityService filters requests for us based on the NetworkCapabilities
+                // passed in the registerNetworkOffer() call.
+                mRequests.add(request);
+                // if the network is already started, this is a no-op.
+                start();
+            }
+
+            @Override
+            public void onNetworkUnneeded(@NonNull NetworkRequest request) {
+                if (DBG) {
+                    Log.d(TAG,
+                            String.format("%s: onNetworkUnneeded for request: %s", name, request));
+                }
+                mRequests.remove(request);
+                if (mRequests.isEmpty()) {
+                    // not currently serving any requests, stop the network.
+                    stop();
+                }
+            }
+        }
+
+        NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
+                @NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities,
+                NetworkProvider networkProvider, Dependencies deps) {
+            name = ifaceName;
+            mIpConfig = Objects.requireNonNull(ipConfig);
+            mCapabilities = Objects.requireNonNull(capabilities);
+            mLegacyType = getLegacyType(mCapabilities);
+            mHandler = handler;
+            mContext = context;
+            mNetworkProvider = networkProvider;
+            mDeps = deps;
+            mNetworkOfferCallback = new EthernetNetworkOfferCallback();
+            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 static NetworkScore getBestNetworkScore() {
+            return new NetworkScore.Builder().build();
+        }
+
+        private void setCapabilities(@NonNull final NetworkCapabilities capabilities) {
+            mCapabilities = new NetworkCapabilities(capabilities);
+            mLegacyType = getLegacyType(mCapabilities);
+
+            if (mLinkUp) {
+                // registering a new network offer will update the existing one, not install a
+                // new one.
+                mNetworkProvider.registerNetworkOffer(getBestNetworkScore(),
+                        new NetworkCapabilities(capabilities), cmd -> mHandler.post(cmd),
+                        mNetworkOfferCallback);
+            }
+        }
+
+        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, mNetworkProvider,
+                    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
+                // retract network offer and stop IpClient.
+                destroy();
+                // If only setting the interface down, send a callback to signal completion.
+                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
+            } else { // was down, goes up
+                // register network offer
+                mNetworkProvider.registerNetworkOffer(getBestNetworkScore(),
+                        new NetworkCapabilities(mCapabilities), (cmd) -> mHandler.post(cmd),
+                        mNetworkOfferCallback);
+            }
+
+            return true;
+        }
+
+        private 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();
+        }
+
+        public void destroy() {
+            mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
+            maybeSendNetworkManagementCallbackForAbort();
+            stop();
+            mRequests.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() + "{ "
+                    + "iface: " + name + ", "
+                    + "up: " + mLinkUp + ", "
+                    + "hwAddress: " + mHwAddress + ", "
+                    + "networkCapabilities: " + mCapabilities + ", "
+                    + "networkAgent: " + mNetworkAgent + ", "
+                    + "ipClient: " + mIpClient + ","
+                    + "linkProperties: " + mLinkProperties
+                    + "}";
+        }
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] 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..71d3e4f
--- /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.EthernetNetworkUpdateRequest;
+import android.net.IEthernetManager;
+import android.net.IEthernetServiceListener;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.ITetheredInterfaceCallback;
+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 enableInterface(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Log.i(TAG, "enableInterface called with: iface=" + iface + ", listener=" + listener);
+        Objects.requireNonNull(iface);
+        throwIfEthernetNotStarted();
+
+        enforceAdminPermission(iface, false, "enableInterface()");
+
+        mTracker.enableInterface(iface, listener);
+    }
+
+    @Override
+    public void disableInterface(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Log.i(TAG, "disableInterface called with: iface=" + iface + ", listener=" + listener);
+        Objects.requireNonNull(iface);
+        throwIfEthernetNotStarted();
+
+        enforceAdminPermission(iface, false, "disableInterface()");
+
+        mTracker.disableInterface(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..6405795
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -0,0 +1,963 @@
+/*
+ * 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.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+";
+
+    /**
+     * 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 {
+        public String getInterfaceRegexFromResource(Context context) {
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getString(
+                    com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
+        }
+
+        public String[] getInterfaceConfigFromResource(Context context) {
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getStringArray(
+                    com.android.connectivity.resources.R.array.config_ethernet_interfaces);
+        }
+    }
+
+    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 = 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 enableInterface(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        mHandler.post(() -> updateInterfaceState(iface, true, listener));
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void disableInterface(@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);
+            // when this broadcast is sent, any calls to notifyTetheredInterfaceAvailable or
+            // notifyTetheredInterfaceUnavailable have already happened
+            broadcastInterfaceStateChange(mDefaultInterface);
+        }
+    }
+
+    private int getInterfaceState(final String iface) {
+        if (mFactory.hasInterface(iface)) {
+            return mFactory.getInterfaceState(iface);
+        }
+        if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
+            // server mode interfaces are not tracked by the factory.
+            // TODO(b/234743836): interface state for server mode interfaces is not tracked
+            // properly; just return link up.
+            return EthernetManager.STATE_LINK_UP;
+        }
+        return EthernetManager.STATE_ABSENT;
+    }
+
+    private int getInterfaceRole(final String iface) {
+        if (mFactory.hasInterface(iface)) {
+            // only client mode interfaces are tracked by the factory.
+            return EthernetManager.ROLE_CLIENT;
+        }
+        if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
+            return EthernetManager.ROLE_SERVER;
+        }
+        return EthernetManager.ROLE_NONE;
+    }
+
+    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);
+        }
+    }
+
+    @VisibleForTesting
+    class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
+
+        @Override
+        public void onInterfaceLinkStateChanged(String iface, boolean up) {
+            if (DBG) {
+                Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up);
+            }
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                updateInterfaceState(iface, up);
+            });
+        }
+
+        @Override
+        public void onInterfaceAdded(String iface) {
+            if (DBG) {
+                Log.i(TAG, "onInterfaceAdded, iface: " + iface);
+            }
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                maybeTrackInterface(iface);
+            });
+        }
+
+        @Override
+        public void onInterfaceRemoved(String iface) {
+            if (DBG) {
+                Log.i(TAG, "onInterfaceRemoved, iface: " + iface);
+            }
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                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 State: "
+                    + (mEthernetState == ETHERNET_STATE_ENABLED ? "enabled" : "disabled"));
+            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..3b44d81
--- /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/netd_shared/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..1cd670a
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -0,0 +1,482 @@
+/*
+ * 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.annotation.NonNull;
+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.IndentingPrintWriter;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.PerUidCounter;
+
+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 LOG = true;
+    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;
+
+    private static final int DUMP_USAGE_REQUESTS_COUNT = 200;
+
+    // The maximum number of request allowed per uid before an exception is thrown.
+    @VisibleForTesting
+    static final int MAX_REQUESTS_PER_UID = 100;
+
+    // All access to this map must be done from the handler thread.
+    // indexed by DataUsageRequest#requestId
+    private final SparseArray<RequestInfo> mDataUsageRequests = new SparseArray<>();
+
+    // Request counters per uid, this is thread safe.
+    private final PerUidCounter mDataUsageRequestsPerUid = new PerUidCounter(MAX_REQUESTS_PER_UID);
+
+    // 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(@NonNull Context context,
+            @NonNull DataUsageRequest inputRequest, @NonNull IUsageCallback callback,
+            int callingPid, int callingUid, @NonNull String callingPackage,
+            @NetworkStatsAccess.Level int accessLevel) {
+        DataUsageRequest request = buildRequest(context, inputRequest, callingUid);
+        RequestInfo requestInfo = buildRequestInfo(request, callback, callingPid, callingUid,
+                callingPackage, accessLevel);
+        if (LOG) Log.d(TAG, "Registering observer for " + requestInfo);
+        mDataUsageRequestsPerUid.incrementCountOrThrow(callingUid);
+
+        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 (LOG) Log.d(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 (LOG) Log.d(TAG, "Unregistering " + requestInfo);
+        mDataUsageRequests.remove(request.requestId);
+        mDataUsageRequestsPerUid.decrementCountOrThrow(requestInfo.mCallingUid);
+        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 callingPid, int callingUid, @NonNull String callingPackage,
+            @NetworkStatsAccess.Level int accessLevel) {
+        if (accessLevel <= NetworkStatsAccess.Level.USER) {
+            return new UserUsageRequestInfo(this, request, callback, callingPid,
+                    callingUid, callingPackage, 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, callingPid,
+                    callingUid, callingPackage, 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 mCallingPid;
+        protected final int mCallingUid;
+        protected final String mCallingPackage;
+        protected final @NetworkStatsAccess.Level int mAccessLevel;
+        protected NetworkStatsRecorder mRecorder;
+        protected NetworkStatsCollection mCollection;
+
+        RequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
+                IUsageCallback callback, int callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+            mStatsObserver = statsObserver;
+            mRequest = request;
+            mCallback = callback;
+            mCallingPid = callingPid;
+            mCallingUid = callingUid;
+            mCallingPackage = callingPackage;
+            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 pid/uid:" + mCallingPid + "/" + mCallingUid
+                    + "(" + mCallingPackage + ")"
+                    + " 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 callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+            super(statsObserver, request, callback, callingPid, callingUid, callingPackage,
+                    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 callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+            super(statsObserver, request, callback, callingPid, callingUid,
+                    callingPackage, 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;
+        }
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        for (int i = 0; i < Math.min(mDataUsageRequests.size(), DUMP_USAGE_REQUESTS_COUNT); i++) {
+            pw.println(mDataUsageRequests.valueAt(i));
+        }
+    }
+}
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..d99e164
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -0,0 +1,558 @@
+/*
+ * 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.annotation.NonNull;
+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.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 #recoverAndDeleteData()}. */
+    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;
+    }
+
+    public long getBucketDuration() {
+        return mBucketDuration;
+    }
+
+    @NonNull
+    public String getCookie() {
+        return mCookie;
+    }
+
+    /**
+     * 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);
+            recoverAndDeleteData();
+        } catch (OutOfMemoryError e) {
+            Log.wtf(TAG, "problem completely reading network stats", e);
+            recoverAndDeleteData();
+        }
+        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);
+                recoverAndDeleteData();
+            } catch (OutOfMemoryError e) {
+                Log.wtf(TAG, "problem persisting pending stats", e);
+                recoverAndDeleteData();
+            }
+        }
+    }
+
+    /**
+     * 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);
+                recoverAndDeleteData();
+            } catch (OutOfMemoryError e) {
+                Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
+                recoverAndDeleteData();
+            }
+        }
+
+        // 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.
+     */
+    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);
+        }
+    }
+
+    /**
+     * 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);
+        }
+    }
+
+    /**
+     * Import a specified {@link NetworkStatsCollection} instance into this recorder,
+     * and write it into a standalone file.
+     * @param collection The target {@link NetworkStatsCollection} instance to be imported.
+     */
+    public void importCollectionLocked(@NonNull NetworkStatsCollection collection)
+            throws IOException {
+        if (mRotator != null) {
+            mRotator.rewriteSingle(new CombiningRewriter(collection), collection.getStartMillis(),
+                    collection.getEndMillis());
+        }
+
+        if (mComplete != null) {
+            throw new IllegalStateException("cannot import data when data already loaded");
+        }
+    }
+
+    /**
+     * Rewriter that will remove any histories or persisted data points before the
+     * specified cutoff time, only writing data back when modified.
+     */
+    public static class RemoveDataBeforeRewriter implements FileRotator.Rewriter {
+        private final NetworkStatsCollection mTemp;
+        private final long mCutoffMills;
+
+        public RemoveDataBeforeRewriter(long bucketDuration, long cutoffMills) {
+            mTemp = new NetworkStatsCollection(bucketDuration);
+            mCutoffMills = cutoffMills;
+        }
+
+        @Override
+        public void reset() {
+            mTemp.reset();
+        }
+
+        @Override
+        public void read(InputStream in) throws IOException {
+            mTemp.read(in);
+            mTemp.clearDirty();
+            mTemp.removeHistoryBefore(mCutoffMills);
+        }
+
+        @Override
+        public boolean shouldWrite() {
+            return mTemp.isDirty();
+        }
+
+        @Override
+        public void write(OutputStream out) throws IOException {
+            mTemp.write(out);
+        }
+    }
+
+    /**
+     * Remove persisted data which contains or is before the cutoff timestamp.
+     */
+    public void removeDataBefore(long cutoffMillis) throws IOException {
+        if (mRotator != null) {
+            try {
+                mRotator.rewriteAll(new RemoveDataBeforeRewriter(
+                        mBucketDuration, cutoffMillis));
+            } catch (IOException e) {
+                Log.wtf(TAG, "problem importing netstats", e);
+                recoverAndDeleteData();
+            } catch (OutOfMemoryError e) {
+                Log.wtf(TAG, "problem importing netstats", e);
+                recoverAndDeleteData();
+            }
+        }
+
+        // Clean up any pending stats
+        if (mPending != null) {
+            mPending.removeHistoryBefore(cutoffMillis);
+        }
+        if (mSinceBoot != null) {
+            mSinceBoot.removeHistoryBefore(cutoffMillis);
+        }
+
+        final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+        if (complete != null) {
+            complete.removeHistoryBefore(cutoffMillis);
+        }
+    }
+
+    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.
+     */
+    void recoverAndDeleteData() {
+        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..63e6501
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -0,0 +1,2974 @@
+/*
+ * 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.ApexEnvironment;
+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.ConnectivityManager;
+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.NetworkStatsDataMigrationUtils;
+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.DeviceConfig;
+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.DeviceConfigUtils;
+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.nio.file.Path;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+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";
+
+    private static final String UID_COUNTERSET_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_uid_counterset_map";
+    private static final String COOKIE_TAG_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
+    private static final String APP_UID_STATS_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_app_uid_stats_map";
+    private static final String STATS_MAP_A_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_stats_map_A";
+    private static final String STATS_MAP_B_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_stats_map_B";
+
+    /**
+     * DeviceConfig flag used to indicate whether the files should be stored in the apex data
+     * directory.
+     */
+    static final String NETSTATS_STORE_FILES_IN_APEXDATA = "netstats_store_files_in_apexdata";
+    /**
+     * DeviceConfig flag is used to indicate whether the legacy files need to be imported, and
+     * retry count before giving up. Only valid when {@link #NETSTATS_STORE_FILES_IN_APEXDATA}
+     * set to true. Note that the value gets rollback when the mainline module gets rollback.
+     */
+    static final String NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS =
+            "netstats_import_legacy_target_attempts";
+    static final int DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS = 1;
+    static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
+    static final String NETSTATS_IMPORT_SUCCESS_COUNTER_NAME = "import.successes";
+
+    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 mStatsDir;
+
+    private final PowerManager.WakeLock mWakeLock;
+
+    private final ContentObserver mContentObserver;
+    private final ContentResolver mContentResolver;
+
+    protected INetd mNetd;
+    private final AlertObserver mAlertObserver = new AlertObserver();
+
+    // Persistent counters that backed by AtomicFile which stored in the data directory as a file,
+    // to track attempts/successes count across reboot. Note that these counter values will be
+    // rollback as the module rollbacks.
+    private PersistentInt mImportLegacyAttemptsCounter = null;
+    private PersistentInt mImportLegacySuccessesCounter = null;
+
+    @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;
+
+    private final Object mOpenSessionCallsLock = new Object();
+    /**
+     * Map from UID to number of opened sessions. This is used for rate-limt an app to open
+     * session frequently
+     */
+    @GuardedBy("mOpenSessionCallsLock")
+    private final SparseIntArray mOpenSessionCallsPerUid = new SparseIntArray();
+    /**
+     * Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
+     * the caller of open session and it is only for debugging.
+     */
+    @GuardedBy("mOpenSessionCallsLock")
+    private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
+
+    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 Clock getDefaultClock() {
+        return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
+                Clock.systemUTC());
+    }
+
+    /**
+     * This class is a key that used in {@code mOpenSessionCallsPerCaller} to identify the count of
+     * the caller.
+     */
+    private static class OpenSessionKey {
+        public final int uid;
+        public final String packageName;
+
+        OpenSessionKey(int uid, @NonNull String packageName) {
+            this.uid = uid;
+            this.packageName = packageName;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            sb.append("uid=").append(uid).append(",");
+            sb.append("package=").append(packageName);
+            sb.append("}");
+            return sb.toString();
+        }
+
+        @Override
+        public boolean equals(@NonNull Object o) {
+            if (this == o) return true;
+            if (o.getClass() != getClass()) return false;
+
+            final OpenSessionKey key = (OpenSessionKey) o;
+            return this.uid == key.uid && TextUtils.equals(this.packageName, key.packageName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(uid, packageName);
+        }
+    }
+
+    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(), 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,
+            @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");
+        mDeps = Objects.requireNonNull(deps, "missing Dependencies");
+        mStatsDir = mDeps.getOrCreateStatsDir();
+        if (!mStatsDir.exists()) {
+            throw new IllegalStateException("Persist data directory does not exist: " + mStatsDir);
+        }
+
+        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 {
+        /**
+         * Get legacy platform stats directory.
+         */
+        @NonNull
+        public File getLegacyStatsDir() {
+            final File systemDataDir = new File(Environment.getDataDirectory(), "system");
+            return new File(systemDataDir, "netstats");
+        }
+
+        /**
+         * Get or create the directory that stores the persisted data usage.
+         */
+        @NonNull
+        public File getOrCreateStatsDir() {
+            final boolean storeInApexDataDir = getStoreFilesInApexData();
+
+            final File statsDataDir;
+            if (storeInApexDataDir) {
+                final File apexDataDir = ApexEnvironment
+                        .getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+                        .getDeviceProtectedDataDir();
+                statsDataDir = new File(apexDataDir, "netstats");
+
+            } else {
+                statsDataDir = getLegacyStatsDir();
+            }
+
+            if (statsDataDir.exists() || statsDataDir.mkdirs()) {
+                return statsDataDir;
+            }
+            throw new IllegalStateException("Cannot write into stats data directory: "
+                    + statsDataDir);
+        }
+
+        /**
+         * Get the count of import legacy target attempts.
+         */
+        public int getImportLegacyTargetAttempts() {
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(
+                    DeviceConfig.NAMESPACE_TETHERING,
+                    NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS,
+                    DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS);
+        }
+
+        /**
+         * Create the persistent counter that counts total import legacy stats attempts.
+         */
+        public PersistentInt createImportLegacyAttemptsCounter(@NonNull Path path)
+                throws IOException {
+            // TODO: Modify PersistentInt to call setStartTime every time a write is made.
+            //  Create and pass a real logger here.
+            return new PersistentInt(path.toString(), null /* logger */);
+        }
+
+        /**
+         * Create the persistent counter that counts total import legacy stats successes.
+         */
+        public PersistentInt createImportLegacySuccessesCounter(@NonNull Path path)
+                throws IOException {
+            return new PersistentInt(path.toString(), null /* logger */);
+        }
+
+        /**
+         * Get the flag of storing files in the apex data directory.
+         * @return whether to store files in the apex data directory.
+         */
+        public boolean getStoreFilesInApexData() {
+            return DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+                    DeviceConfig.NAMESPACE_TETHERING,
+                    NETSTATS_STORE_FILES_IN_APEXDATA, true);
+        }
+
+        /**
+         * Read legacy persisted network stats from disk.
+         */
+        @NonNull
+        public NetworkStatsCollection readPlatformCollection(
+                @NonNull String prefix, long bucketDuration) throws IOException {
+            return NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, bucketDuration);
+        }
+
+        /**
+         * 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, mStatsDir);
+            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir);
+            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir);
+            mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+                    mStatsDir);
+
+            updatePersistThresholdsLocked();
+
+            // upgrade any legacy stats
+            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,
+            File baseDir) {
+        final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
+                Context.DROPBOX_SERVICE);
+        return new NetworkStatsRecorder(new FileRotator(
+                baseDir, 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;
+    }
+
+    private static class MigrationInfo {
+        public final NetworkStatsRecorder recorder;
+        public NetworkStatsCollection collection;
+        public boolean imported;
+        MigrationInfo(@NonNull final NetworkStatsRecorder recorder) {
+            this.recorder = recorder;
+            collection = null;
+            imported = false;
+        }
+    }
+
+    @GuardedBy("mStatsLock")
+    private void maybeUpgradeLegacyStatsLocked() {
+        final boolean storeFilesInApexData = mDeps.getStoreFilesInApexData();
+        if (!storeFilesInApexData) {
+            return;
+        }
+        try {
+            mImportLegacyAttemptsCounter = mDeps.createImportLegacyAttemptsCounter(
+                    mStatsDir.toPath().resolve(NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME));
+            mImportLegacySuccessesCounter = mDeps.createImportLegacySuccessesCounter(
+                    mStatsDir.toPath().resolve(NETSTATS_IMPORT_SUCCESS_COUNTER_NAME));
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+            return;
+        }
+
+        final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
+        final int attempts;
+        try {
+            attempts = mImportLegacyAttemptsCounter.get();
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to read attempts counter, skip.", e);
+            return;
+        }
+        if (attempts >= targetAttempts) return;
+
+        Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts);
+
+        final MigrationInfo[] migrations = new MigrationInfo[]{
+                new MigrationInfo(mDevRecorder), new MigrationInfo(mXtRecorder),
+                new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
+        };
+
+        // Legacy directories will be created by recorders if they do not exist
+        final File legacyBaseDir = mDeps.getLegacyStatsDir();
+        final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
+                buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir),
+                buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir),
+                buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir),
+                buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir)
+        };
+
+        long migrationEndTime = Long.MIN_VALUE;
+        boolean endedWithFallback = false;
+        try {
+            // First, read all legacy collections. This is OEM code and it can throw. Don't
+            // commit any data to disk until all are read.
+            for (int i = 0; i < migrations.length; i++) {
+                final MigrationInfo migration = migrations[i];
+                migration.collection = readPlatformCollectionForRecorder(migration.recorder);
+
+                // Also read the collection with legacy method
+                final NetworkStatsRecorder legacyRecorder = legacyRecorders[i];
+
+                final NetworkStatsCollection legacyStats;
+                try {
+                    legacyStats = legacyRecorder.getOrLoadCompleteLocked();
+                } catch (Throwable e) {
+                    Log.wtf(TAG, "Failed to read stats with legacy method", e);
+                    // Newer stats will be used here; that's the only thing that is usable
+                    continue;
+                }
+
+                String errMsg;
+                Throwable exception = null;
+                try {
+                    errMsg = compareStats(migration.collection, legacyStats);
+                } catch (Throwable e) {
+                    errMsg = "Failed to compare migrated stats with all stats";
+                    exception = e;
+                }
+
+                if (errMsg != null) {
+                    Log.wtf(TAG, "NetworkStats import for migration " + i
+                            + " returned invalid data: " + errMsg, exception);
+                    // Fall back to legacy stats for this boot. The stats for old data will be
+                    // re-imported again on next boot until they succeed the import. This is fine
+                    // since every import clears the previous stats for the imported timespan.
+                    migration.collection = legacyStats;
+                    endedWithFallback = true;
+                }
+            }
+
+            // Find the latest end time.
+            for (final MigrationInfo migration : migrations) {
+                final long migrationEnd = migration.collection.getEndMillis();
+                if (migrationEnd > migrationEndTime) migrationEndTime = migrationEnd;
+            }
+
+            // Reading all collections from legacy data has succeeded. At this point it is
+            // safe to start overwriting the files on disk. The next step is to remove all
+            // data in the new location that overlaps with imported data. This ensures that
+            // any data in the new location that was created by a previous failed import is
+            // ignored. After that, write the imported data into the recorder. The code
+            // below can still possibly throw (disk error or OutOfMemory for example), but
+            // does not depend on code from non-mainline code.
+            Log.i(TAG, "Rewriting data with imported collections with cutoff "
+                    + Instant.ofEpochMilli(migrationEndTime));
+            for (final MigrationInfo migration : migrations) {
+                migration.imported = true;
+                migration.recorder.removeDataBefore(migrationEndTime);
+                if (migration.collection.isEmpty()) continue;
+                migration.recorder.importCollectionLocked(migration.collection);
+            }
+
+            if (endedWithFallback) {
+                Log.wtf(TAG, "Imported platform collections with legacy fallback");
+            } else {
+                Log.i(TAG, "Successfully imported platform collections");
+            }
+        } catch (Throwable e) {
+            // The code above calls OEM code that may behave differently across devices.
+            // It can throw any exception including RuntimeExceptions and
+            // OutOfMemoryErrors. Try to recover anyway.
+            Log.wtf(TAG, "Platform data import failed. Remaining tries "
+                    + (targetAttempts - attempts), e);
+
+            // Failed this time around : try again next time unless we're out of tries.
+            try {
+                mImportLegacyAttemptsCounter.set(attempts + 1);
+            } catch (IOException ex) {
+                Log.wtf(TAG, "Failed to update attempts counter.", ex);
+            }
+
+            // Try to remove any data from the failed import.
+            if (migrationEndTime > Long.MIN_VALUE) {
+                try {
+                    for (final MigrationInfo migration : migrations) {
+                        if (migration.imported) {
+                            migration.recorder.removeDataBefore(migrationEndTime);
+                        }
+                    }
+                } catch (Throwable f) {
+                    // If rollback still throws, there isn't much left to do. Try nuking
+                    // all data, since that's the last stop. If nuking still throws, the
+                    // framework will reboot, and if there are remaining tries, the migration
+                    // process will retry, which is fine because it's idempotent.
+                    for (final MigrationInfo migration : migrations) {
+                        migration.recorder.recoverAndDeleteData();
+                    }
+                }
+            }
+
+            return;
+        }
+
+        // Success ! No need to import again next time.
+        try {
+            mImportLegacyAttemptsCounter.set(targetAttempts);
+            // The successes counter is only for debugging. Hence, the synchronization
+            // between these two counters are not very critical.
+            final int successCount = mImportLegacySuccessesCounter.get();
+            mImportLegacySuccessesCounter.set(successCount + 1);
+        } catch (IOException e) {
+            Log.wtf(TAG, "Succeed but failed to update counters.", e);
+        }
+    }
+
+    private static String str(NetworkStatsCollection.Key key) {
+        StringBuilder sb = new StringBuilder()
+                .append(key.ident.toString())
+                .append(" uid=").append(key.uid);
+        if (key.set != SET_FOREGROUND) {
+            sb.append(" set=").append(key.set);
+        }
+        if (key.tag != 0) {
+            sb.append(" tag=").append(key.tag);
+        }
+        return sb.toString();
+    }
+
+    // The importer will modify some keys when importing them.
+    // In order to keep the comparison code simple, add such special cases here and simply
+    // ignore them. This should not impact fidelity much because the start/end checks and the total
+    // bytes check still need to pass.
+    private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) {
+        if (key.ident.isEmpty()) return false;
+        final NetworkIdentity firstIdent = key.ident.iterator().next();
+
+        // Non-mobile network with non-empty RAT type.
+        // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed
+        // in, but it looks like it was previously possible to persist it to disk. The importer sets
+        // the RAT type to NETWORK_TYPE_ALL.
+        if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE
+                && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Nullable
+    private static String compareStats(
+            NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
+                migrated.getEntries();
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
+
+        final ArraySet<NetworkStatsCollection.Key> unmatchedLegKeys =
+                new ArraySet<>(legEntries.keySet());
+
+        for (NetworkStatsCollection.Key legKey : legEntries.keySet()) {
+            final NetworkStatsHistory legHistory = legEntries.get(legKey);
+            final NetworkStatsHistory migHistory = migEntries.get(legKey);
+
+            if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+                unmatchedLegKeys.remove(legKey);
+                continue;
+            }
+
+            if (migHistory == null) {
+                return "Missing migrated history for legacy key " + str(legKey)
+                        + ", legacy history was " + legHistory;
+            }
+            if (!migHistory.isSameAs(legHistory)) {
+                return "Difference in history for key " + legKey + "; legacy history " + legHistory
+                        + ", migrated history " + migHistory;
+            }
+            unmatchedLegKeys.remove(legKey);
+        }
+
+        if (!unmatchedLegKeys.isEmpty()) {
+            final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0));
+            return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size()
+                    + ", first unmatched collection " + first;
+        }
+
+        if (migrated.getStartMillis() != legacy.getStartMillis()
+                || migrated.getEndMillis() != legacy.getEndMillis()) {
+            return "Start / end of the collections "
+                    + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and "
+                    + migrated.getEndMillis() + "/" + legacy.getEndMillis()
+                    + " don't match";
+        }
+
+        if (migrated.getTotalBytes() != legacy.getTotalBytes()) {
+            return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes()
+                    + " don't match for collections with start/end "
+                    + migrated.getStartMillis()
+                    + "/" + legacy.getStartMillis();
+        }
+
+        return null;
+    }
+
+    @GuardedBy("mStatsLock")
+    @NonNull
+    private NetworkStatsCollection readPlatformCollectionForRecorder(
+            @NonNull final NetworkStatsRecorder rec) throws IOException {
+        final String prefix = rec.getCookie();
+        Log.i(TAG, "Importing platform collection for prefix " + prefix);
+        final NetworkStatsCollection collection = Objects.requireNonNull(
+                mDeps.readPlatformCollection(prefix, rec.getBucketDuration()),
+                "Imported platform collection for prefix " + prefix + " must not be null");
+
+        final long bootTimestamp = System.currentTimeMillis() - SystemClock.elapsedRealtime();
+        if (!collection.isEmpty() && bootTimestamp < collection.getStartMillis()) {
+            throw new IllegalArgumentException("Platform collection for prefix " + prefix
+                    + " contains data that could not possibly come from the previous boot "
+                    + "(start timestamp = " + Instant.ofEpochMilli(collection.getStartMillis())
+                    + ", last booted at " + Instant.ofEpochMilli(bootTimestamp));
+        }
+
+        Log.i(TAG, "Successfully read platform collection spanning from "
+                // Instant uses ISO-8601 for toString()
+                + Instant.ofEpochMilli(collection.getStartMillis()).toString() + " to "
+                + Instant.ofEpochMilli(collection.getEndMillis()).toString());
+        return collection;
+    }
+
+    /**
+     * 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(@NonNull OpenSessionKey key) {
+        final long lastCallTime;
+        final long now = SystemClock.elapsedRealtime();
+
+        synchronized (mOpenSessionCallsLock) {
+            Integer callsPerCaller = mOpenSessionCallsPerCaller.get(key);
+            if (callsPerCaller == null) {
+                mOpenSessionCallsPerCaller.put((key), 1);
+            } else {
+                mOpenSessionCallsPerCaller.put(key, Integer.sum(callsPerCaller, 1));
+            }
+
+            int callsPerUid = mOpenSessionCallsPerUid.get(key.uid, 0);
+            mOpenSessionCallsPerUid.put(key.uid, callsPerUid + 1);
+
+            if (key.uid == android.os.Process.SYSTEM_UID) {
+                return false;
+            }
+
+            // To avoid a non-system user to be rate-limited after system users open sessions,
+            // so update mLastStatsSessionPoll after checked if the uid is SYSTEM_UID.
+            lastCallTime = mLastStatsSessionPoll;
+            mLastStatsSessionPoll = now;
+        }
+
+        return now - lastCallTime < POLL_RATE_LIMIT_MS;
+    }
+
+    private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) {
+        // 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();
+        final OpenSessionKey key = new OpenSessionKey(callingUid, callingPackage);
+        flags = isRateLimitedForPoll(key)
+                ? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN)
+                : flags;
+        return flags;
+    }
+
+    private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
+        final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage);
+        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");
+
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
+        @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
+        DataUsageRequest normalizedRequest;
+        final long token = Binder.clearCallingIdentity();
+        try {
+            normalizedRequest = mStatsObservers.register(mContext,
+                    request, callback, callingPid, callingUid, callingPackage, 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);
+        }
+
+       // TODO: Remove the UID's entries from mOpenSessionCallsPerUid and
+       // mOpenSessionCallsPerCaller
+    }
+
+    /**
+     * 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("Directory:");
+            pw.increaseIndent();
+            pw.println(mStatsDir);
+            pw.decreaseIndent();
+
+            pw.println("Configs:");
+            pw.increaseIndent();
+            pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
+            pw.println();
+            pw.print(NETSTATS_STORE_FILES_IN_APEXDATA, mDeps.getStoreFilesInApexData());
+            pw.println();
+            pw.print(NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS, mDeps.getImportLegacyTargetAttempts());
+            pw.println();
+            if (mDeps.getStoreFilesInApexData()) {
+                try {
+                    pw.print("platform legacy stats import attempts count",
+                            mImportLegacyAttemptsCounter.get());
+                    pw.println();
+                    pw.print("platform legacy stats import successes count",
+                            mImportLegacySuccessesCounter.get());
+                    pw.println();
+                } catch (IOException e) {
+                    pw.println("(failed to dump platform legacy stats import counters)");
+                }
+            }
+
+            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 HashMap calls;
+            synchronized (mOpenSessionCallsLock) {
+                calls = new HashMap<>(mOpenSessionCallsPerCaller);
+            }
+            final List<Map.Entry<OpenSessionKey, Integer>> list = new ArrayList<>(calls.entrySet());
+            Collections.sort(list,
+                    (left, right) -> Integer.compare(left.getValue(), right.getValue()));
+            final int num = list.size();
+            final int end = Math.max(0, num - DUMP_STATS_SESSION_COUNT);
+            pw.println("Top openSession callers:");
+            pw.increaseIndent();
+            for (int j = num - 1; j >= end; j--) {
+                final Map.Entry<OpenSessionKey, Integer> entry = list.get(j);
+                pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+
+            }
+            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();
+
+            pw.println("Stats Observers:");
+            pw.increaseIndent();
+            mStatsObservers.dump(pw);
+            pw.decreaseIndent();
+            pw.println();
+
+            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/PersistentInt.java b/service-t/src/com/android/server/net/PersistentInt.java
new file mode 100644
index 0000000..c212b77
--- /dev/null
+++ b/service-t/src/com/android/server/net/PersistentInt.java
@@ -0,0 +1,108 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.AtomicFile;
+import android.util.SystemConfigFileCommitEventLogger;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * A simple integer backed by an on-disk {@link AtomicFile}. Not thread-safe.
+ */
+public class PersistentInt {
+    private final String mPath;
+    private final AtomicFile mFile;
+
+    /**
+     * Constructs a new {@code PersistentInt}. The counter is set to 0 if the file does not exist.
+     * Before returning, the constructor checks that the file is readable and writable. This
+     * indicates that in the future {@link #get} and {@link #set} are likely to succeed,
+     * though other events (data corruption, other code deleting the file, etc.) may cause these
+     * calls to fail in the future.
+     *
+     * @param path the path of the file to use.
+     * @param logger the logger
+     * @throws IOException the counter could not be read or written
+     */
+    public PersistentInt(@NonNull String path, @Nullable SystemConfigFileCommitEventLogger logger)
+            throws IOException {
+        mPath = path;
+        mFile = new AtomicFile(new File(path), logger);
+        checkReadWrite();
+    }
+
+    private void checkReadWrite() throws IOException {
+        int value;
+        try {
+            value = get();
+        } catch (FileNotFoundException e) {
+            // Counter does not exist. Attempt to initialize to 0.
+            // Note that we cannot tell here if the file does not exist or if opening it failed,
+            // because in Java both of those throw FileNotFoundException.
+            value = 0;
+        }
+        set(value);
+        get();
+        // No exceptions? Good.
+    }
+
+    /**
+      * Gets the current value.
+      *
+      * @return the current value of the counter.
+      * @throws IOException if reading the value failed.
+      */
+    public int get() throws IOException {
+        try (FileInputStream fin = mFile.openRead();
+             DataInputStream din = new DataInputStream(fin)) {
+            return din.readInt();
+        }
+    }
+
+    /**
+     * Sets the current value.
+     * @param value the value to set
+     * @throws IOException if writing the value failed.
+     */
+    public void set(int value) throws IOException {
+        FileOutputStream fout = null;
+        try {
+            fout = mFile.startWrite();
+            DataOutputStream dout = new DataOutputStream(fout);
+            dout.writeInt(value);
+            mFile.finishWrite(fout);
+        } catch (IOException e) {
+            if (fout != null) {
+                mFile.failWrite(fout);
+            }
+            throw e;
+        }
+    }
+
+    public String getPath() {
+        return mPath;
+    }
+}
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 0e6fe92..91b9d1c 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",
@@ -109,6 +158,7 @@
     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",
@@ -119,7 +169,7 @@
         "networkstack-client",
         "PlatformProperties",
         "service-connectivity-protos",
-        "NetworkStackApiCurrentShims",
+        "NetworkStackApiStableShims",
     ],
     apex_available: [
         "com.android.tethering",
@@ -166,6 +216,11 @@
     apex_available: [
         "com.android.tethering",
     ],
+    optimize: {
+        enabled: true,
+        shrink: true,
+        proguard_flags_files: ["proguard.flags"],
+    },
     lint: { strict_updatability_linting: true },
 }
 
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index f491cc7..02b2875 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -23,6 +23,7 @@
     name: "ServiceConnectivityResources",
     sdk_version: "module_30",
     min_sdk_version: "30",
+    target_sdk_version: "33",
     resource_dirs: [
         "res",
     ],
diff --git a/service/ServiceConnectivityResources/res/values-af/strings.xml b/service/ServiceConnectivityResources/res/values-af/strings.xml
index 550ab8a..086c6e3 100644
--- a/service/ServiceConnectivityResources/res/values-af/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-af/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Stelselkonnektiwiteithulpbronne"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Meld aan by Wi-Fi-netwerk"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Meld by netwerk aan"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Stelselkonnektiwiteithulpbronne"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Meld aan by Wi-Fi-netwerk"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Meld by netwerk aan"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het geen internettoegang nie"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tik vir opsies"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Selnetwerk het nie internettoegang nie"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Netwerk het nie internettoegang nie"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Daar kan nie by private DNS-bediener ingegaan word nie"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het beperkte konnektiwiteit"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tik om in elk geval te koppel"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Het oorgeskakel na <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Toestel gebruik <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wanneer <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> geen internettoegang het nie. Heffings kan geld."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Het oorgeskakel van <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het geen internettoegang nie"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tik vir opsies"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Selnetwerk het nie internettoegang nie"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Netwerk het nie internettoegang nie"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Daar kan nie by private DNS-bediener ingegaan word nie"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het beperkte konnektiwiteit"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tik om in elk geval te koppel"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Het oorgeskakel na <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Toestel gebruik <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wanneer <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> geen internettoegang het nie. Heffings kan geld."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Het oorgeskakel van <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobiele data"</item>
-    <item msgid="6341719431034774569">"Wi-fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobiele data"</item>
+    <item msgid="5624324321165953608">"Wi-fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"\'n onbekende netwerktipe"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"\'n onbekende netwerktipe"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-am/strings.xml b/service/ServiceConnectivityResources/res/values-am/strings.xml
index 7f1a9db..886b353 100644
--- a/service/ServiceConnectivityResources/res/values-am/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-am/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"የስርዓት ግንኙነት መርጃዎች"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ወደ Wi-Fi አውታረ መረብ በመለያ ግባ"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ወደ አውታረ መረብ በመለያ ይግቡ"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"የስርዓት ግንኙነት መርጃዎች"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ወደ Wi-Fi አውታረ መረብ በመለያ ግባ"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ወደ አውታረ መረብ በመለያ ይግቡ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ምንም የበይነ መረብ መዳረሻ የለም"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ለአማራጮች መታ ያድርጉ"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"የተንቀሳቃሽ ስልክ አውታረ መረብ የበይነመረብ መዳረሻ የለውም"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"አውታረ መረብ የበይነመረብ መዳረሻ የለውም"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"የግል ዲኤንኤስ አገልጋይ ሊደረስበት አይችልም"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> የተገደበ ግንኙነት አለው"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ለማንኛውም ለማገናኘት መታ ያድርጉ"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"ወደ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ተቀይሯል"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ምንም ዓይነት የበይነመረብ ግንኙነት በማይኖረው ጊዜ መሣሪያዎች <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ን ይጠቀማሉ። ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ።"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"ከ<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ወደ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ተቀይሯል"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ምንም የበይነ መረብ መዳረሻ የለም"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ለአማራጮች መታ ያድርጉ"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"የተንቀሳቃሽ ስልክ አውታረ መረብ የበይነመረብ መዳረሻ የለውም"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"አውታረ መረብ የበይነመረብ መዳረሻ የለውም"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"የግል ዲኤንኤስ አገልጋይ ሊደረስበት አይችልም"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> የተገደበ ግንኙነት አለው"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"ለማንኛውም ለማገናኘት መታ ያድርጉ"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"ወደ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ተቀይሯል"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ምንም ዓይነት የበይነመረብ ግንኙነት በማይኖረው ጊዜ መሣሪያዎች <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ን ይጠቀማሉ። ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ።"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"ከ<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ወደ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ተቀይሯል"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"የተንቀሳቃሽ ስልክ ውሂብ"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"ብሉቱዝ"</item>
-    <item msgid="1160736166977503463">"ኢተርኔት"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"የተንቀሳቃሽ ስልክ ውሂብ"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"ብሉቱዝ"</item>
+    <item msgid="346574747471703768">"ኢተርኔት"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"አንድ ያልታወቀ አውታረ መረብ ዓይነት"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"አንድ ያልታወቀ አውታረ መረብ ዓይነት"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ar/strings.xml b/service/ServiceConnectivityResources/res/values-ar/strings.xml
index b7a62c5..07d9c2e 100644
--- a/service/ServiceConnectivityResources/res/values-ar/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ar/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"مصادر إمكانية اتصال الخادم"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"‏تسجيل الدخول إلى شبكة Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"تسجيل الدخول إلى الشبكة"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"مصادر إمكانية اتصال الخادم"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"‏تسجيل الدخول إلى شبكة Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"تسجيل الدخول إلى الشبكة"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"لا يتوفّر في <xliff:g id="NETWORK_SSID">%1$s</xliff:g> إمكانية الاتصال بالإنترنت."</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"انقر للحصول على الخيارات."</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"شبكة الجوّال هذه غير متصلة بالإنترنت"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"الشبكة غير متصلة بالإنترنت"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"لا يمكن الوصول إلى خادم أسماء نظام نطاقات خاص"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"إمكانية اتصال <xliff:g id="NETWORK_SSID">%1$s</xliff:g> محدودة."</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"يمكنك النقر للاتصال على أي حال."</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"تم التبديل إلى <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"يستخدم الجهاز <xliff:g id="NEW_NETWORK">%1$s</xliff:g> عندما لا يتوفر اتصال بالإنترنت في شبكة <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>، ويمكن أن يتم فرض رسوم مقابل ذلك."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"تم التبديل من <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> إلى <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"لا يتوفّر في <xliff:g id="NETWORK_SSID">%1$s</xliff:g> إمكانية الاتصال بالإنترنت."</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"انقر للحصول على الخيارات."</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"شبكة الجوّال هذه غير متصلة بالإنترنت"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"الشبكة غير متصلة بالإنترنت"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"لا يمكن الوصول إلى خادم أسماء نظام نطاقات خاص"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"إمكانية اتصال <xliff:g id="NETWORK_SSID">%1$s</xliff:g> محدودة."</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"يمكنك النقر للاتصال على أي حال."</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"تم التبديل إلى <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"يستخدم الجهاز <xliff:g id="NEW_NETWORK">%1$s</xliff:g> عندما لا يتوفر اتصال بالإنترنت في شبكة <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>، ويمكن أن يتم فرض رسوم مقابل ذلك."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"تم التبديل من <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> إلى <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"بيانات الجوّال"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"بلوتوث"</item>
-    <item msgid="1160736166977503463">"إيثرنت"</item>
-    <item msgid="7347618872551558605">"‏شبكة افتراضية خاصة (VPN)"</item>
+    <item msgid="3004933964374161223">"بيانات الجوّال"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"بلوتوث"</item>
+    <item msgid="346574747471703768">"إيثرنت"</item>
+    <item msgid="5734728378097476003">"‏شبكة افتراضية خاصة (VPN)"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"نوع شبكة غير معروف"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"نوع شبكة غير معروف"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml
index cf8e6ac..e753cb3 100644
--- a/service/ServiceConnectivityResources/res/values-as/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-as/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ছিষ্টেম সংযোগৰ উৎস"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"নেটৱৰ্কত ছাইন ইন কৰক"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ছিষ্টেম সংযোগৰ উৎস"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"নেটৱৰ্কত ছাইন ইন কৰক"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"অধিক বিকল্পৰ বাবে টিপক"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"ম’বাইল নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ব্যক্তিগত DNS ছাৰ্ভাৰ এক্সেছ কৰিব নোৱাৰি"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ সকলো সেৱাৰ এক্সেছ নাই"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"যিকোনো প্ৰকাৰে সংযোগ কৰিবলৈ টিপক"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>লৈ সলনি কৰা হ’ল"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"যেতিয়া <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ত ইণ্টাৰনেট নাথাকে, তেতিয়া ডিভাইচে <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ক ব্যৱহাৰ কৰে। মাচুল প্ৰযোজ্য হ\'ব পাৰে।"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>ৰ পৰা <xliff:g id="NEW_NETWORK">%2$s</xliff:g> লৈ সলনি কৰা হ’ল"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"অধিক বিকল্পৰ বাবে টিপক"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"ম’বাইল নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"ব্যক্তিগত DNS ছাৰ্ভাৰ এক্সেছ কৰিব নোৱাৰি"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ সকলো সেৱাৰ এক্সেছ নাই"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"যিকোনো প্ৰকাৰে সংযোগ কৰিবলৈ টিপক"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>লৈ সলনি কৰা হ’ল"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"যেতিয়া <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ত ইণ্টাৰনেট নাথাকে, তেতিয়া ডিভাইচে <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ক ব্যৱহাৰ কৰে। মাচুল প্ৰযোজ্য হ\'ব পাৰে।"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>ৰ পৰা <xliff:g id="NEW_NETWORK">%2$s</xliff:g> লৈ সলনি কৰা হ’ল"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"ম’বাইল ডেটা"</item>
-    <item msgid="6341719431034774569">"ৱাই-ফাই"</item>
-    <item msgid="5081440868800877512">"ব্লুটুথ"</item>
-    <item msgid="1160736166977503463">"ইথাৰনেট"</item>
-    <item msgid="7347618872551558605">"ভিপিএন"</item>
+    <item msgid="3004933964374161223">"ম’বাইল ডেটা"</item>
+    <item msgid="5624324321165953608">"ৱাই-ফাই"</item>
+    <item msgid="5667906231066981731">"ব্লুটুথ"</item>
+    <item msgid="346574747471703768">"ইথাৰনেট"</item>
+    <item msgid="5734728378097476003">"ভিপিএন"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"অজ্ঞাত প্ৰকাৰৰ নেটৱৰ্ক"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"অজ্ঞাত প্ৰকাৰৰ নেটৱৰ্ক"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-az/strings.xml b/service/ServiceConnectivityResources/res/values-az/strings.xml
index 7e927ed..f33a3e3 100644
--- a/service/ServiceConnectivityResources/res/values-az/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-az/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sistem Bağlantı Resursları"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi şəbəkəsinə daxil ol"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Şəbəkəyə daxil olun"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Sistem Bağlantı Resursları"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi şəbəkəsinə daxil ol"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Şəbəkəyə daxil olun"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> üçün internet girişi əlçatan deyil"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Seçimlər üçün tıklayın"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobil şəbəkənin internetə girişi yoxdur"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Şəbəkənin internetə girişi yoxdur"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Özəl DNS serverinə giriş mümkün deyil"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> bağlantını məhdudlaşdırdı"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"İstənilən halda klikləyin"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> şəbəkə növünə keçirildi"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> şəbəkəsinin internetə girişi olmadıqda, cihaz <xliff:g id="NEW_NETWORK">%1$s</xliff:g> şəbəkəsini istifadə edir. Xidmət haqqı tutula bilər."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> şəbəkəsindən <xliff:g id="NEW_NETWORK">%2$s</xliff:g> şəbəkəsinə keçirildi"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> üçün internet girişi əlçatan deyil"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Seçimlər üçün tıklayın"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobil şəbəkənin internetə girişi yoxdur"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Şəbəkənin internetə girişi yoxdur"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Özəl DNS serverinə giriş mümkün deyil"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> bağlantını məhdudlaşdırdı"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"İstənilən halda klikləyin"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> şəbəkə növünə keçirildi"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> şəbəkəsinin internetə girişi olmadıqda, cihaz <xliff:g id="NEW_NETWORK">%1$s</xliff:g> şəbəkəsini istifadə edir. Xidmət haqqı tutula bilər."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> şəbəkəsindən <xliff:g id="NEW_NETWORK">%2$s</xliff:g> şəbəkəsinə keçirildi"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobil data"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobil data"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"naməlum şəbəkə növü"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"naməlum şəbəkə növü"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml
index 3f1b976..7398e7c 100644
--- a/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resursi za povezivanje sa sistemom"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prijavljivanje na WiFi mrežu"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Prijavite se na mrežu"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Resursi za povezivanje sa sistemom"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Prijavljivanje na WiFi mrežu"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Prijavite se na mrežu"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Dodirnite za opcije"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilna mreža nema pristup internetu"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Mreža nema pristup internetu"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Pristup privatnom DNS serveru nije uspeo"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu vezu"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Dodirnite da biste se ipak povezali"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Prešli ste na tip mreže <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Uređaj koristi tip mreže <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kada tip mreže <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu. Možda će se naplaćivati troškovi."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Prešli ste sa tipa mreže <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na tip mreže <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Dodirnite za opcije"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilna mreža nema pristup internetu"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Mreža nema pristup internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Pristup privatnom DNS serveru nije uspeo"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu vezu"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Dodirnite da biste se ipak povezali"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Prešli ste na tip mreže <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Uređaj koristi tip mreže <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kada tip mreže <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu. Možda će se naplaćivati troškovi."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Prešli ste sa tipa mreže <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na tip mreže <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobilni podaci"</item>
-    <item msgid="6341719431034774569">"WiFi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Eternet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobilni podaci"</item>
+    <item msgid="5624324321165953608">"WiFi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Eternet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nepoznat tip mreže"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"nepoznat tip mreže"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-be/strings.xml b/service/ServiceConnectivityResources/res/values-be/strings.xml
index 21edf24..3459cc7 100644
--- a/service/ServiceConnectivityResources/res/values-be/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-be/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Рэсурсы для падключэння да сістэмы"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Уваход у сетку Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Увайдзіце ў сетку"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Рэсурсы для падключэння да сістэмы"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Уваход у сетку Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Увайдзіце ў сетку"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> не мае доступу ў інтэрнэт"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Дакраніцеся, каб убачыць параметры"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мабільная сетка не мае доступу ў інтэрнэт"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Сетка не мае доступу ў інтэрнэт"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Не ўдалося атрымаць доступ да прыватнага DNS-сервера"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> мае абмежаваную магчымасць падключэння"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Націсніце, каб падключыцца"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Выкананы пераход да <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Прылада выкарыстоўвае сетку <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, калі ў сетцы <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> няма доступу да інтэрнэту. Можа спаганяцца плата."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Выкананы пераход з <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> да <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> не мае доступу ў інтэрнэт"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Дакраніцеся, каб убачыць параметры"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мабільная сетка не мае доступу ў інтэрнэт"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Сетка не мае доступу ў інтэрнэт"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Не ўдалося атрымаць доступ да прыватнага DNS-сервера"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> мае абмежаваную магчымасць падключэння"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Націсніце, каб падключыцца"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Выкананы пераход да <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Прылада выкарыстоўвае сетку <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, калі ў сетцы <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> няма доступу да інтэрнэту. Можа спаганяцца плата."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Выкананы пераход з <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> да <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мабільная перадача даных"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мабільная перадача даных"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"невядомы тып сеткі"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"невядомы тып сеткі"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-bg/strings.xml b/service/ServiceConnectivityResources/res/values-bg/strings.xml
index c3c2d72..b4ae618 100644
--- a/service/ServiceConnectivityResources/res/values-bg/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-bg/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ресурси за свързаността на системата"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Влизане в Wi-Fi мрежа"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Вход в мрежата"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Ресурси за свързаността на системата"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Влизане в Wi-Fi мрежа"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Вход в мрежата"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> няма достъп до интернет"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Докоснете за опции"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мобилната мрежа няма достъп до интернет"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Мрежата няма достъп до интернет"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Не може да се осъществи достъп до частния DNS сървър"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничена свързаност"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Докоснете, за да се свържете въпреки това"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Превключи се към <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Устройството използва <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, когато <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> няма достъп до интернет. Възможно е да бъдете таксувани."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Превключи се от <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> към <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> няма достъп до интернет"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Докоснете за опции"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобилната мрежа няма достъп до интернет"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Мрежата няма достъп до интернет"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Не може да се осъществи достъп до частния DNS сървър"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничена свързаност"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Докоснете, за да се свържете въпреки това"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Превключи се към <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Устройството използва <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, когато <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> няма достъп до интернет. Възможно е да бъдете таксувани."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Превключи се от <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> към <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мобилни данни"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мобилни данни"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"неизвестен тип мрежа"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"неизвестен тип мрежа"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-bn/strings.xml b/service/ServiceConnectivityResources/res/values-bn/strings.xml
index 0f693bd..3b32973 100644
--- a/service/ServiceConnectivityResources/res/values-bn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-bn/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"সিস্টেম কানেক্টিভিটি রিসোর্সেস"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ওয়াই-ফাই নেটওয়ার্কে সাইন-ইন করুন"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"নেটওয়ার্কে সাইন-ইন করুন"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"সিস্টেম কানেক্টিভিটি রিসোর্সেস"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ওয়াই-ফাই নেটওয়ার্কে সাইন-ইন করুন"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"নেটওয়ার্কে সাইন-ইন করুন"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-এর ইন্টারনেটে অ্যাক্সেস নেই"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"বিকল্পগুলির জন্য আলতো চাপুন"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"মোবাইল নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ব্যক্তিগত ডিএনএস সার্ভার অ্যাক্সেস করা যাবে না"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-এর সীমিত কানেক্টিভিটি আছে"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"তবুও কানেক্ট করতে ট্যাপ করুন"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> এ পাল্টানো হয়েছে"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> এ ইন্টারনেট অ্যাক্সেস না থাকলে <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ব্যবহার করা হয়৷ ডেটা চার্জ প্রযোজ্য৷"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> থেকে <xliff:g id="NEW_NETWORK">%2$s</xliff:g> এ পাল্টানো হয়েছে"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-এর ইন্টারনেটে অ্যাক্সেস নেই"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"বিকল্পগুলির জন্য আলতো চাপুন"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"মোবাইল নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"ব্যক্তিগত ডিএনএস সার্ভার অ্যাক্সেস করা যাবে না"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-এর সীমিত কানেক্টিভিটি আছে"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"তবুও কানেক্ট করতে ট্যাপ করুন"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> এ পাল্টানো হয়েছে"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> এ ইন্টারনেট অ্যাক্সেস না থাকলে <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ব্যবহার করা হয়৷ ডেটা চার্জ প্রযোজ্য৷"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> থেকে <xliff:g id="NEW_NETWORK">%2$s</xliff:g> এ পাল্টানো হয়েছে"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"মোবাইল ডেটা"</item>
-    <item msgid="6341719431034774569">"ওয়াই-ফাই"</item>
-    <item msgid="5081440868800877512">"ব্লুটুথ"</item>
-    <item msgid="1160736166977503463">"ইথারনেট"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"মোবাইল ডেটা"</item>
+    <item msgid="5624324321165953608">"ওয়াই-ফাই"</item>
+    <item msgid="5667906231066981731">"ব্লুটুথ"</item>
+    <item msgid="346574747471703768">"ইথারনেট"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"এই নেটওয়ার্কের ধরন অজানা"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"এই নেটওয়ার্কের ধরন অজানা"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-bs/strings.xml b/service/ServiceConnectivityResources/res/values-bs/strings.xml
index 33d6ed9..0bc0a7c 100644
--- a/service/ServiceConnectivityResources/res/values-bs/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-bs/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Izvori povezivosti sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prijavljivanje na WiFi mrežu"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Prijava na mrežu"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Izvori povezivosti sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Prijavljivanje na WiFi mrežu"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Prijava na mrežu"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Dodirnite za opcije"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilna mreža nema pristup internetu"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Mreža nema pristup internetu"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Nije moguće pristupiti privatnom DNS serveru"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu povezivost"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Dodirnite da se ipak povežete"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Prebačeno na: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Kada <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu, uređaj koristi mrežu <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Moguća je naplata usluge."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Prebačeno iz mreže <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> u <xliff:g id="NEW_NETWORK">%2$s</xliff:g> mrežu"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Dodirnite za opcije"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilna mreža nema pristup internetu"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Mreža nema pristup internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Nije moguće pristupiti privatnom DNS serveru"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu povezivost"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Dodirnite da se ipak povežete"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Prebačeno na: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Kada <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu, uređaj koristi mrežu <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Moguća je naplata usluge."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Prebačeno iz mreže <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> u <xliff:g id="NEW_NETWORK">%2$s</xliff:g> mrežu"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"prijenos podataka na mobilnoj mreži"</item>
-    <item msgid="6341719431034774569">"WiFi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"prijenos podataka na mobilnoj mreži"</item>
+    <item msgid="5624324321165953608">"WiFi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nepoznata vrsta mreže"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"nepoznata vrsta mreže"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ca/strings.xml b/service/ServiceConnectivityResources/res/values-ca/strings.xml
index 04f6bd2..22b9dbd 100644
--- a/service/ServiceConnectivityResources/res/values-ca/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ca/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de connectivitat del sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Inicia la sessió a la xarxa Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Inicia la sessió a la xarxa"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Recursos de connectivitat del sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Inicia la sessió a la xarxa Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Inicia la sessió a la xarxa"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no té accés a Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toca per veure les opcions"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"La xarxa mòbil no té accés a Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"La xarxa no té accés a Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"No es pot accedir al servidor DNS privat"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> té una connectivitat limitada"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toca per connectar igualment"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Actualment en ús: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"El dispositiu utilitza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> en cas que <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tingui accés a Internet. És possible que s\'hi apliquin càrrecs."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Abans es feia servir la xarxa <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>; ara s\'utilitza <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no té accés a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toca per veure les opcions"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"La xarxa mòbil no té accés a Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"La xarxa no té accés a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"No es pot accedir al servidor DNS privat"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> té una connectivitat limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Toca per connectar igualment"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Actualment en ús: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"El dispositiu utilitza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> en cas que <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tingui accés a Internet. És possible que s\'hi apliquin càrrecs."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Abans es feia servir la xarxa <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>; ara s\'utilitza <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"dades mòbils"</item>
-    <item msgid="6341719431034774569">"Wi‑Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"dades mòbils"</item>
+    <item msgid="5624324321165953608">"Wi‑Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tipus de xarxa desconegut"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"un tipus de xarxa desconegut"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-cs/strings.xml b/service/ServiceConnectivityResources/res/values-cs/strings.xml
index 6309e78..ccf21ee 100644
--- a/service/ServiceConnectivityResources/res/values-cs/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-cs/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Zdroje pro připojení systému"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Přihlásit se k síti Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Přihlásit se k síti"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Zdroje pro připojení systému"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Přihlásit se k síti Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Přihlásit se k síti"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Síť <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá přístup k internetu"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Klepnutím zobrazíte možnosti"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilní síť nemá přístup k internetu"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Síť nemá přístup k internetu"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Nelze získat přístup k soukromému serveru DNS"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Síť <xliff:g id="NETWORK_SSID">%1$s</xliff:g> umožňuje jen omezené připojení"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Klepnutím se i přesto připojíte"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Přechod na síť <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Když síť <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nebude mít přístup k internetu, zařízení použije síť <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Mohou být účtovány poplatky."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Přechod ze sítě <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na síť <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Síť <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá přístup k internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Klepnutím zobrazíte možnosti"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilní síť nemá přístup k internetu"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Síť nemá přístup k internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Nelze získat přístup k soukromému serveru DNS"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Síť <xliff:g id="NETWORK_SSID">%1$s</xliff:g> umožňuje jen omezené připojení"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Klepnutím se i přesto připojíte"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Přechod na síť <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Když síť <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nebude mít přístup k internetu, zařízení použije síť <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Mohou být účtovány poplatky."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Přechod ze sítě <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na síť <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobilní data"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobilní data"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"neznámý typ sítě"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"neznámý typ sítě"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-da/strings.xml b/service/ServiceConnectivityResources/res/values-da/strings.xml
index 57c58af..a33143e 100644
--- a/service/ServiceConnectivityResources/res/values-da/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-da/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Systemets forbindelsesressourcer"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Log ind på Wi-Fi-netværk"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Log ind på netværk"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Systemets forbindelsesressourcer"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Log ind på Wi-Fi-netværk"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Log ind på netværk"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetforbindelse"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tryk for at se valgmuligheder"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilnetværket har ingen internetadgang"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Netværket har ingen internetadgang"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Der er ikke adgang til den private DNS-server"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begrænset forbindelse"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tryk for at oprette forbindelse alligevel"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Der blev skiftet til <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Enheden benytter <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, når der ikke er internetadgang via <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Der opkræves muligvis betaling."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Der blev skiftet fra <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> til <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetforbindelse"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tryk for at se valgmuligheder"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilnetværket har ingen internetadgang"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Netværket har ingen internetadgang"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Der er ikke adgang til den private DNS-server"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begrænset forbindelse"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tryk for at oprette forbindelse alligevel"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Der blev skiftet til <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Enheden benytter <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, når der ikke er internetadgang via <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Der opkræves muligvis betaling."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Der blev skiftet fra <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> til <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobildata"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobildata"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"en ukendt netværkstype"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"en ukendt netværkstype"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml
index d0c2551..96cc7d2 100644
--- a/service/ServiceConnectivityResources/res/values-de/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-de/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Systemverbindungsressourcen"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"In WLAN anmelden"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Im Netzwerk anmelden"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Systemverbindungsressourcen"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"In WLAN anmelden"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Im Netzwerk anmelden"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> hat keinen Internetzugriff"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Für Optionen tippen"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiles Netzwerk hat keinen Internetzugriff"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Netzwerk hat keinen Internetzugriff"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Schlechte Verbindung mit <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tippen, um die Verbindung trotzdem herzustellen"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Zu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> gewechselt"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Auf dem Gerät werden <xliff:g id="NEW_NETWORK">%1$s</xliff:g> genutzt, wenn über <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> kein Internet verfügbar ist. Eventuell fallen Gebühren an."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Von \"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>\" zu \"<xliff:g id="NEW_NETWORK">%2$s</xliff:g>\" gewechselt"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> hat keinen Internetzugriff"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Für Optionen tippen"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiles Netzwerk hat keinen Internetzugriff"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Netzwerk hat keinen Internetzugriff"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Schlechte Verbindung mit <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tippen, um die Verbindung trotzdem herzustellen"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Zu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> gewechselt"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Auf dem Gerät werden <xliff:g id="NEW_NETWORK">%1$s</xliff:g> genutzt, wenn über <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> kein Internet verfügbar ist. Eventuell fallen Gebühren an."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Von \"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>\" zu \"<xliff:g id="NEW_NETWORK">%2$s</xliff:g>\" gewechselt"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"Mobile Daten"</item>
-    <item msgid="6341719431034774569">"WLAN"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"Mobile Daten"</item>
+    <item msgid="5624324321165953608">"WLAN"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ein unbekannter Netzwerktyp"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ein unbekannter Netzwerktyp"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-el/strings.xml b/service/ServiceConnectivityResources/res/values-el/strings.xml
index 1c2838d..b5f319d 100644
--- a/service/ServiceConnectivityResources/res/values-el/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-el/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Πόροι συνδεσιμότητας συστήματος"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Συνδεθείτε στο δίκτυο Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Σύνδεση στο δίκτυο"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Πόροι συνδεσιμότητας συστήματος"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Συνδεθείτε στο δίκτυο Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Σύνδεση στο δίκτυο"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Η εφαρμογή <xliff:g id="NETWORK_SSID">%1$s</xliff:g> δεν έχει πρόσβαση στο διαδίκτυο"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Πατήστε για να δείτε τις επιλογές"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Το δίκτυο κινητής τηλεφωνίας δεν έχει πρόσβαση στο διαδίκτυο."</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Το δίκτυο δεν έχει πρόσβαση στο διαδίκτυο."</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Δεν είναι δυνατή η πρόσβαση στον ιδιωτικό διακομιστή DNS."</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Το δίκτυο <xliff:g id="NETWORK_SSID">%1$s</xliff:g> έχει περιορισμένη συνδεσιμότητα"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Πατήστε για σύνδεση ούτως ή άλλως"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Μετάβαση σε δίκτυο <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Η συσκευή χρησιμοποιεί το δίκτυο <xliff:g id="NEW_NETWORK">%1$s</xliff:g> όταν το δίκτυο <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> δεν έχει πρόσβαση στο διαδίκτυο. Μπορεί να ισχύουν χρεώσεις."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Μετάβαση από το δίκτυο <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> στο δίκτυο <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Η εφαρμογή <xliff:g id="NETWORK_SSID">%1$s</xliff:g> δεν έχει πρόσβαση στο διαδίκτυο"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Πατήστε για να δείτε τις επιλογές"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Το δίκτυο κινητής τηλεφωνίας δεν έχει πρόσβαση στο διαδίκτυο."</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Το δίκτυο δεν έχει πρόσβαση στο διαδίκτυο."</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Δεν είναι δυνατή η πρόσβαση στον ιδιωτικό διακομιστή DNS."</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Το δίκτυο <xliff:g id="NETWORK_SSID">%1$s</xliff:g> έχει περιορισμένη συνδεσιμότητα"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Πατήστε για σύνδεση ούτως ή άλλως"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Μετάβαση σε δίκτυο <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Η συσκευή χρησιμοποιεί το δίκτυο <xliff:g id="NEW_NETWORK">%1$s</xliff:g> όταν το δίκτυο <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> δεν έχει πρόσβαση στο διαδίκτυο. Μπορεί να ισχύουν χρεώσεις."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Μετάβαση από το δίκτυο <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> στο δίκτυο <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"δεδομένα κινητής τηλεφωνίας"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"δεδομένα κινητής τηλεφωνίας"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"άγνωστος τύπος δικτύου"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"άγνωστος τύπος δικτύου"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml
index db5ad70..c490cf8 100644
--- a/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System connectivity resources"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Sign in to a Wi-Fi network"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Sign in to network"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"System connectivity resources"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Sign in to a Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Sign in to network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tap for options"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobile network has no Internet access"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Network has no Internet access"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Private DNS server cannot be accessed"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tap to connect anyway"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tap for options"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobile network has no Internet access"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Network has no Internet access"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Private DNS server cannot be accessed"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tap to connect anyway"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobile data"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobile data"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"an unknown network type"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"an unknown network type"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml
index db5ad70..c490cf8 100644
--- a/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System connectivity resources"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Sign in to a Wi-Fi network"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Sign in to network"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"System connectivity resources"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Sign in to a Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Sign in to network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tap for options"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobile network has no Internet access"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Network has no Internet access"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Private DNS server cannot be accessed"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tap to connect anyway"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tap for options"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobile network has no Internet access"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Network has no Internet access"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Private DNS server cannot be accessed"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tap to connect anyway"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobile data"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobile data"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"an unknown network type"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"an unknown network type"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml
index db5ad70..c490cf8 100644
--- a/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System connectivity resources"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Sign in to a Wi-Fi network"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Sign in to network"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"System connectivity resources"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Sign in to a Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Sign in to network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tap for options"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobile network has no Internet access"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Network has no Internet access"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Private DNS server cannot be accessed"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tap to connect anyway"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tap for options"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobile network has no Internet access"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Network has no Internet access"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Private DNS server cannot be accessed"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tap to connect anyway"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobile data"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobile data"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"an unknown network type"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"an unknown network type"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml
index db5ad70..c490cf8 100644
--- a/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System connectivity resources"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Sign in to a Wi-Fi network"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Sign in to network"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"System connectivity resources"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Sign in to a Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Sign in to network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tap for options"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobile network has no Internet access"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Network has no Internet access"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Private DNS server cannot be accessed"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tap to connect anyway"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tap for options"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobile network has no Internet access"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Network has no Internet access"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Private DNS server cannot be accessed"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tap to connect anyway"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobile data"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobile data"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"an unknown network type"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"an unknown network type"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml
index 2602bfa..67c3659 100644
--- a/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‎‎‎‏‏‏‎‏‏‎‎‎‏‎‎‎‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‏‏‏‎‎‏‏‎‏‏‏‎‏‎‏‏‏‎‎‎‏‎‏‏‎System Connectivity Resources‎‏‎‎‏‎"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‎‏‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‏‎‏‎‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‏‎‎‏‎Sign in to Wi-Fi network‎‏‎‎‏‎"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‏‎‏‎‏‏‎‎‏‎‏‎‎‏‏‎‎‏‏‎‏‏‏‏‏‏‎‎‎‏‎‏‎‎‎‏‏‏‎‎‎‎‎‏‏‎‏‎‎‏‏‎‎‎‎Sign in to network‎‏‎‎‏‎"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‎‏‎‏‏‏‎‏‎‏‏‏‎‎‏‏‎‎‎‎‏‏‏‎‏‏‏‎‎‎‏‏‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‎‏‏‏‏‎‎System Connectivity Resources‎‏‎‎‏‎"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‎‎‎‎‏‎‎‎‏‏‎‎‏‏‎‎‎‎‏‎‏‎‎‏‎‎‏‏‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎Sign in to Wi-Fi network‎‏‎‎‏‎"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‎‎‏‎‎‎‎‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‏‏‏‏‎‏‎‎‏‎Sign in to network‎‏‎‎‏‎"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‎‏‏‏‎‎‏‎‎‏‏‎‏‏‎‎‏‏‎‏‏‏‎‏‏‏‏‎‎‎‏‏‏‏‏‎‎‏‎‎‎‏‎‎‏‏‎<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎‏‎‎‏‏‏‎ has no internet access‎‏‎‎‏‎"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‎‎‎‎‏‏‏‎‏‎‎‎‎‏‎‏‏‏‏‎‏‏‎‏‎‎‏‏‏‏‎‏‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‎‎‏‎‏‎‎‎‎Tap for options‎‏‎‎‏‎"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‎‏‏‎‎‏‏‎‎‏‏‎‎‎‎‏‎‏‏‏‏‎‏‎‏‎‏‎‎‏‎‎‏‎‎‏‎‎‏‏‎‏‎‏‏‏‎Mobile network has no internet access‎‏‎‎‏‎"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‎‏‎‎‎‏‎‏‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‎‏‏‎‏‎‏‎‎‎‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‏‎Network has no internet access‎‏‎‎‏‎"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‏‎‏‏‎‎‏‎‎‎‎‏‏‏‎‏‏‎‏‎‏‎‎‎‎‏‎‏‎‏‏‎‎‏‏‏‎‎‎‎‎‏‎Private DNS server cannot be accessed‎‏‎‎‏‎"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‏‎‏‎‏‏‏‎‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‏‏‎‎‎‏‏‏‎‏‎‏‏‏‎‏‎‎‏‎‏‎‏‎‎‎‎‏‎‎‏‎‎‏‎‎‏‏‎<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎‏‎‎‏‏‏‎ has limited connectivity‎‏‎‎‏‎"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‏‏‏‎‏‏‎‏‏‎‎‏‏‏‏‏‏‎‎‏‏‎‏‏‏‎‏‎‏‎‎‎‎‎‎‎‎‏‎‏‎‎‏‎‏‎‏‎‏‎‏‎‎Tap to connect anyway‎‏‎‎‏‎"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‎‏‎‎‎‎‎‎‏‎‏‏‎‎‏‎‏‎‎‏‎‎‏‎‏‎‏‏‎‎‎‏‎‎‎‏‏‎‎‏‏‏‎‏‏‏‎‎‏‏‎‎‎‎‎Switched to ‎‏‎‎‏‏‎<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‎‎‏‏‎‎‏‏‏‎‏‎‏‎‏‎‏‏‎‏‏‏‎‎‏‏‎‏‏‏‎‎‏‏‎‏‎‎‎‏‎‎‎‏‏‏‎‎‏‎‏‎‎‎‏‎‏‎Device uses ‎‏‎‎‏‏‎<xliff:g id="NEW_NETWORK">%1$s</xliff:g>‎‏‎‎‏‏‏‎ when ‎‏‎‎‏‏‎<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>‎‏‎‎‏‏‏‎ has no internet access. Charges may apply.‎‏‎‎‏‎"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‏‏‏‏‎‎‏‎‏‎‏‏‏‏‎‎‎‎‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‏‎‎‏‎‎‏‎‏‏‎‎‎‎‏‎‎‎‏‎Switched from ‎‏‎‎‏‏‎<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>‎‏‎‎‏‏‏‎ to ‎‏‎‎‏‏‎<xliff:g id="NEW_NETWORK">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‏‏‎‏‎‎‎‎‎‏‎‎‎‏‏‎‎‎‎‎‏‏‏‎‎‎‎‏‎‏‎‎‏‎‎‎‎‏‏‏‎‎‏‎‏‎‎‏‏‎‏‎‎‏‏‎‎‏‎‎‏‏‎<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎‏‎‎‏‏‏‎ has no internet access‎‏‎‎‏‎"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‏‏‏‎‏‏‏‏‎‏‏‎‏‎‏‎‎‏‏‎‏‏‎‏‎‎‏‎‎‏‎‏‎‏‏‎‎‎‏‏‎‏‎‎Tap for options‎‏‎‎‏‎"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‎‏‏‏‎‎‏‎‎‏‎‏‎‏‎‎‏‎‎‎‎‏‎‎‏‎‏‎‏‎‎Mobile network has no internet access‎‏‎‎‏‎"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‎‎‎‎‏‎‎‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‏‎‎‎‏‏‎‏‎‏‏‎‎‏‎‏‎‏‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‎Network has no internet access‎‏‎‎‏‎"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‏‎‎‏‏‏‎‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‎‎‏‏‏‏‏‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‎‏‏‎‎‎‏‏‏‏‎Private DNS server cannot be accessed‎‏‎‎‏‎"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‏‎‎‎‎‎‎‏‏‏‏‎‎‏‏‎‎‎‏‎‏‏‎‎‎‎‏‏‎‎‎‏‎‏‎‏‎‏‎‎‎‏‎‏‎‏‏‏‎‎‏‏‎‏‎‏‎‎‎‏‎‎‏‏‎<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎‏‎‎‏‏‏‎ has limited connectivity‎‏‎‎‏‎"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‏‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‏‎‏‎‏‏‎‏‏‎‏‏‎‏‏‎‎‎‎‏‎‎‎‎‎‏‎‏‏‎‏‏‏‏‎‏‎‏‎Tap to connect anyway‎‏‎‎‏‎"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‎‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‏‎‎‏‎‎‎‏‏‎‎‏‎‎‏‏‎‎‎‏‏‎‎‎‏‏‎‏‏‏‏‎Switched to ‎‏‎‎‏‏‎<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‎‏‏‏‎‎‏‎‏‏‎‏‎‎‏‏‏‏‎‏‎‎‏‏‏‏‎‏‎‏‎‎‎‎‎‏‎‏‎‏‏‎‏‏‎‏‎‎‎‏‎‏‎‎‎‎‎Device uses ‎‏‎‎‏‏‎<xliff:g id="NEW_NETWORK">%1$s</xliff:g>‎‏‎‎‏‏‏‎ when ‎‏‎‎‏‏‎<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>‎‏‎‎‏‏‏‎ has no internet access. Charges may apply.‎‏‎‎‏‎"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‏‎‏‎‎‏‏‏‎‎‏‎‎‎‎‏‎‎‏‎‏‎‏‎‎‏‎‎‏‎‏‏‎‏‎‏‎‎‏‏‏‏‏‎Switched from ‎‏‎‎‏‏‎<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>‎‏‎‎‏‏‏‎ to ‎‏‎‎‏‏‎<xliff:g id="NEW_NETWORK">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‏‏‏‎‏‏‎‎‏‎‏‎‏‏‎mobile data‎‏‎‎‏‎"</item>
-    <item msgid="6341719431034774569">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‎‎‎‏‎‎‏‎‏‎‎‎‎‎‎‏‏‏‎‎‏‎‎‎‎‎‎‎‎‎‎‎‎‎‏‎‏‎‎‏‎‎‏‎‎‎‎‏‎‏‎‎‏‎Wi-Fi‎‏‎‎‏‎"</item>
-    <item msgid="5081440868800877512">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‎‎‎‎‏‎‎‏‏‏‎‎‏‏‏‏‎‎‎‏‏‎‎‎‎‏‎‏‏‎‎‎‎‎‎‎‏‏‏‎‏‎‏‎‎‏‏‏‏‎‎‏‎‎‎‎Bluetooth‎‏‎‎‏‎"</item>
-    <item msgid="1160736166977503463">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‏‏‎‏‏‎‎‏‎‎‏‏‎‏‏‏‏‎‏‎‏‎‎‏‎‎‏‎‎‎‎‎‎‎‏‏‏‎‎‏‏‏‎Ethernet‎‏‎‎‏‎"</item>
-    <item msgid="7347618872551558605">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‏‏‎‏‏‎‎‏‎‎‎‏‎‎‏‏‎‏‏‏‎‎‏‏‎‏‎VPN‎‏‎‎‏‎"</item>
+    <item msgid="3004933964374161223">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‏‎‏‏‎‎‏‏‏‎‏‎‏‎‏‏‏‎‎‎‎‏‎‏‎‎‎‏‎‎‎‎‎‎‎‎‎‏‏‏‎‎‏‏‎‏‏‏‎‏‎‎‎‏‏‏‎mobile data‎‏‎‎‏‎"</item>
+    <item msgid="5624324321165953608">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‎‎‏‏‎‏‏‎‎‏‏‏‎‏‎‎‏‎‏‎‏‏‏‏‎‏‎‎‎‎‏‎‏‎‏‏‏‏‏‏‎‎‏‎‏‎‎‏‎‎‏‎‎‎‎Wi-Fi‎‏‎‎‏‎"</item>
+    <item msgid="5667906231066981731">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‏‎‏‎‎‎‎‏‏‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‎‎‎‏‏‏‎‏‎‏‎‏‏‎‎‎‏‏‎Bluetooth‎‏‎‎‏‎"</item>
+    <item msgid="346574747471703768">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‏‏‎‎‏‏‏‏‎‏‎‎‎‏‏‏‏‏‏‎‎‏‎‏‏‎‎‎‎‏‏‏‎‎‏‎‏‎‏‏‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‎‎Ethernet‎‏‎‎‏‎"</item>
+    <item msgid="5734728378097476003">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‎‎‏‎‏‎‏‏‏‎‏‏‎‎‏‎‎‎‏‎‎‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‎‏‎‎‎‏‎‎‎‎‏‏‎‏‎‎‎‏‏‎VPN‎‏‎‎‏‎"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‏‏‏‎‎‏‎‏‏‎‏‎‏‏‏‏‎‏‎‏‏‎‎‏‏‏‎‏‎‏‎‏‎‎‏‎‏‏‎‎‏‎‏‎‏‏‎‏‏‏‏‎‎‎an unknown network type‎‏‎‎‏‎"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‎‎‎‎‎‎‏‎‏‎‎‎‏‏‎‏‎‏‏‎‏‏‎‏‎‏‏‎‏‎‎‎‏‎‎‎‏‎‎‎‏‏‏‎‏‏‎‏‏‏‏‎‎‏‎‎an unknown network type‎‏‎‎‏‎"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
index e5f1833..fdca468 100644
--- a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividad del sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Accede a una red Wi-Fi."</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Acceder a la red"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Recursos de conectividad del sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Accede a una red Wi-Fi."</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Acceder a la red"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>no tiene acceso a Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Presiona para ver opciones"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"La red móvil no tiene acceso a Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"La red no tiene acceso a Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"No se puede acceder al servidor DNS privado"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiene conectividad limitada"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Presiona para conectarte de todas formas"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Se cambió a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"El dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cuando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tiene acceso a Internet. Es posible que se apliquen cargos."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Se cambió de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>no tiene acceso a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Presiona para ver opciones"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"La red móvil no tiene acceso a Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"La red no tiene acceso a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"No se puede acceder al servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiene conectividad limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Presiona para conectarte de todas formas"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Se cambió a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"El dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cuando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tiene acceso a Internet. Es posible que se apliquen cargos."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Se cambió de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"Datos móviles"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"Datos móviles"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tipo de red desconocido"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"un tipo de red desconocido"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-es/strings.xml b/service/ServiceConnectivityResources/res/values-es/strings.xml
index e4f4307..f4a7e3d 100644
--- a/service/ServiceConnectivityResources/res/values-es/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-es/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividad del sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Iniciar sesión en red Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Iniciar sesión en la red"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Recursos de conectividad del sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Iniciar sesión en red Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Iniciar sesión en la red"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no tiene acceso a Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toca para ver opciones"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"La red móvil no tiene acceso a Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"La red no tiene acceso a Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"No se ha podido acceder al servidor DNS privado"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiene una conectividad limitada"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toca para conectarte de todas formas"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Se ha cambiado a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"El dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cuando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tiene acceso a Internet. Es posible que se apliquen cargos."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Se ha cambiado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no tiene acceso a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toca para ver opciones"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"La red móvil no tiene acceso a Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"La red no tiene acceso a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"No se ha podido acceder al servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiene una conectividad limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Toca para conectarte de todas formas"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Se ha cambiado a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"El dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cuando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tiene acceso a Internet. Es posible que se apliquen cargos."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Se ha cambiado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"datos móviles"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"datos móviles"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tipo de red desconocido"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"un tipo de red desconocido"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-et/strings.xml b/service/ServiceConnectivityResources/res/values-et/strings.xml
index cec408f..cf997b3 100644
--- a/service/ServiceConnectivityResources/res/values-et/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-et/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Süsteemi ühenduvuse allikad"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Logi sisse WiFi-võrku"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Võrku sisselogimine"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Süsteemi ühenduvuse allikad"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Logi sisse WiFi-võrku"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Võrku sisselogimine"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Võrgul <xliff:g id="NETWORK_SSID">%1$s</xliff:g> puudub Interneti-ühendus"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Puudutage valikute nägemiseks"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiilsidevõrgul puudub Interneti-ühendus"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Võrgul puudub Interneti-ühendus"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Privaatsele DNS-serverile ei pääse juurde"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Võrgu <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ühendus on piiratud"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Puudutage, kui soovite siiski ühenduse luua"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Lülitati võrgule <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Seade kasutab võrku <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, kui võrgul <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> puudub juurdepääs Internetile. Rakenduda võivad tasud."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Lülitati võrgult <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> võrgule <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Võrgul <xliff:g id="NETWORK_SSID">%1$s</xliff:g> puudub Interneti-ühendus"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Puudutage valikute nägemiseks"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiilsidevõrgul puudub Interneti-ühendus"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Võrgul puudub Interneti-ühendus"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Privaatsele DNS-serverile ei pääse juurde"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Võrgu <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ühendus on piiratud"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Puudutage, kui soovite siiski ühenduse luua"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Lülitati võrgule <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Seade kasutab võrku <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, kui võrgul <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> puudub juurdepääs Internetile. Rakenduda võivad tasud."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Lülitati võrgult <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> võrgule <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobiilne andmeside"</item>
-    <item msgid="6341719431034774569">"WiFi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobiilne andmeside"</item>
+    <item msgid="5624324321165953608">"WiFi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"tundmatu võrgutüüp"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"tundmatu võrgutüüp"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml
index f3ee9b1..2c4e431 100644
--- a/service/ServiceConnectivityResources/res/values-eu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sistemaren konexio-baliabideak"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Hasi saioa Wi-Fi sarean"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Hasi saioa sarean"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Sistemaren konexio-baliabideak"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Hasi saioa Wi-Fi sarean"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Hasi saioa sarean"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Ezin da konektatu Internetera <xliff:g id="NETWORK_SSID">%1$s</xliff:g> sarearen bidez"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Sakatu aukerak ikusteko"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Sare mugikorra ezin da konektatu Internetera"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Sarea ezin da konektatu Internetera"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ezin da atzitu DNS zerbitzari pribatua"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> sareak konektagarritasun murriztua du"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Sakatu hala ere konektatzeko"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> erabiltzen ari zara orain"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> Internetera konektatzeko gauza ez denean, <xliff:g id="NEW_NETWORK">%1$s</xliff:g> erabiltzen du gailuak. Agian kostuak ordaindu beharko dituzu."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> erabiltzen ari zinen, baina <xliff:g id="NEW_NETWORK">%2$s</xliff:g> erabiltzen ari zara orain"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Ezin da konektatu Internetera <xliff:g id="NETWORK_SSID">%1$s</xliff:g> sarearen bidez"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Sakatu aukerak ikusteko"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Sare mugikorra ezin da konektatu Internetera"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Sarea ezin da konektatu Internetera"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Ezin da atzitu DNS zerbitzari pribatua"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> sareak konektagarritasun murriztua du"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Sakatu hala ere konektatzeko"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> erabiltzen ari zara orain"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> Internetera konektatzeko gauza ez denean, <xliff:g id="NEW_NETWORK">%1$s</xliff:g> erabiltzen du gailuak. Baliteke zerbait ordaindu behar izatea."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> erabiltzen ari zinen, baina <xliff:g id="NEW_NETWORK">%2$s</xliff:g> erabiltzen ari zara orain"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"datu-konexioa"</item>
-    <item msgid="6341719431034774569">"Wifia"</item>
-    <item msgid="5081440868800877512">"Bluetooth-a"</item>
-    <item msgid="1160736166977503463">"Ethernet-a"</item>
-    <item msgid="7347618872551558605">"VPNa"</item>
+    <item msgid="3004933964374161223">"datu-konexioa"</item>
+    <item msgid="5624324321165953608">"Wifia"</item>
+    <item msgid="5667906231066981731">"Bluetooth-a"</item>
+    <item msgid="346574747471703768">"Ethernet-a"</item>
+    <item msgid="5734728378097476003">"VPNa"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"sare mota ezezaguna"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"sare mota ezezaguna"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-fa/strings.xml b/service/ServiceConnectivityResources/res/values-fa/strings.xml
index 0c5b147..296ce8e 100644
--- a/service/ServiceConnectivityResources/res/values-fa/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fa/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"منابع اتصال سیستم"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"‏ورود به شبکه Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ورود به سیستم شبکه"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"منابع اتصال سیستم"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"‏ورود به شبکه Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ورود به سیستم شبکه"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> به اینترنت دسترسی ندارد"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"برای گزینه‌ها ضربه بزنید"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"شبکه تلفن همراه به اینترنت دسترسی ندارد"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"شبکه به اینترنت دسترسی ندارد"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"‏سرور DNS خصوصی قابل دسترسی نیست"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> اتصال محدودی دارد"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"به‌هرصورت، برای اتصال ضربه بزنید"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"به <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> تغییر کرد"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"وقتی <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> به اینترنت دسترسی نداشته باشد، دستگاه از <xliff:g id="NEW_NETWORK">%1$s</xliff:g> استفاده می‌کند. ممکن است هزینه‌هایی اعمال شود."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"از <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> به <xliff:g id="NEW_NETWORK">%2$s</xliff:g> تغییر کرد"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> به اینترنت دسترسی ندارد"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"برای گزینه‌ها ضربه بزنید"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"شبکه تلفن همراه به اینترنت دسترسی ندارد"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"شبکه به اینترنت دسترسی ندارد"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"‏سرور DNS خصوصی قابل دسترسی نیست"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> اتصال محدودی دارد"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"به‌هرصورت، برای اتصال ضربه بزنید"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"به <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> تغییر کرد"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"وقتی <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> به اینترنت دسترسی نداشته باشد، دستگاه از <xliff:g id="NEW_NETWORK">%1$s</xliff:g> استفاده می‌کند. ممکن است هزینه‌هایی اعمال شود."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"از <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> به <xliff:g id="NEW_NETWORK">%2$s</xliff:g> تغییر کرد"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"داده تلفن همراه"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"بلوتوث"</item>
-    <item msgid="1160736166977503463">"اترنت"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"داده تلفن همراه"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"بلوتوث"</item>
+    <item msgid="346574747471703768">"اترنت"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"نوع شبکه نامشخص"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"نوع شبکه نامشخص"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-fi/strings.xml b/service/ServiceConnectivityResources/res/values-fi/strings.xml
index 84c0034..07d2907 100644
--- a/service/ServiceConnectivityResources/res/values-fi/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fi/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Järjestelmän yhteysresurssit"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Kirjaudu Wi-Fi-verkkoon"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Kirjaudu verkkoon"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Järjestelmän yhteysresurssit"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Kirjaudu Wi-Fi-verkkoon"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Kirjaudu verkkoon"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ei ole yhteydessä internetiin"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Näytä vaihtoehdot napauttamalla."</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiiliverkko ei ole yhteydessä internetiin"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Verkko ei ole yhteydessä internetiin"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ei pääsyä yksityiselle DNS-palvelimelle"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> toimii rajoitetulla yhteydellä"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Yhdistä napauttamalla"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> otettiin käyttöön"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> otetaan käyttöön, kun <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ei voi muodostaa yhteyttä internetiin. Veloitukset ovat mahdollisia."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> poistettiin käytöstä ja <xliff:g id="NEW_NETWORK">%2$s</xliff:g> otettiin käyttöön."</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ei ole yhteydessä internetiin"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Näytä vaihtoehdot napauttamalla."</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiiliverkko ei ole yhteydessä internetiin"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Verkko ei ole yhteydessä internetiin"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Ei pääsyä yksityiselle DNS-palvelimelle"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> toimii rajoitetulla yhteydellä"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Yhdistä napauttamalla"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> otettiin käyttöön"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> otetaan käyttöön, kun <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ei voi muodostaa yhteyttä internetiin. Veloitukset ovat mahdollisia."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> poistettiin käytöstä ja <xliff:g id="NEW_NETWORK">%2$s</xliff:g> otettiin käyttöön."</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobiilidata"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobiilidata"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"tuntematon verkon tyyppi"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"tuntematon verkon tyyppi"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml
index 0badf1b..7d5b366 100644
--- a/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ressources de connectivité système"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Connectez-vous au réseau Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Connectez-vous au réseau"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Ressources de connectivité système"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Connectez-vous au réseau Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Connectez-vous au réseau"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Le réseau <xliff:g id="NETWORK_SSID">%1$s</xliff:g> n\'offre aucun accès à Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Touchez pour afficher les options"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Le réseau cellulaire n\'offre aucun accès à Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Le réseau n\'offre aucun accès à Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Impossible d\'accéder au serveur DNS privé"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Le réseau <xliff:g id="NETWORK_SSID">%1$s</xliff:g> offre une connectivité limitée"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Touchez pour vous connecter quand même"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Passé au réseau <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"L\'appareil utilise <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quand <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> n\'a pas d\'accès à Internet. Des frais peuvent s\'appliquer."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Passé du réseau <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> au <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Le réseau <xliff:g id="NETWORK_SSID">%1$s</xliff:g> n\'offre aucun accès à Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Touchez pour afficher les options"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Le réseau cellulaire n\'offre aucun accès à Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Le réseau n\'offre aucun accès à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Impossible d\'accéder au serveur DNS privé"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Le réseau <xliff:g id="NETWORK_SSID">%1$s</xliff:g> offre une connectivité limitée"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Touchez pour vous connecter quand même"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Passé au réseau <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"L\'appareil utilise <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quand <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> n\'a pas d\'accès à Internet. Des frais peuvent s\'appliquer."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Passé du réseau <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> au <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"données cellulaires"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"RPV"</item>
+    <item msgid="3004933964374161223">"données cellulaires"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"RPV"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un type de réseau inconnu"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"un type de réseau inconnu"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-fr/strings.xml b/service/ServiceConnectivityResources/res/values-fr/strings.xml
index b483525..2331d9b 100644
--- a/service/ServiceConnectivityResources/res/values-fr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fr/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ressources de connectivité système"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Connectez-vous au réseau Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Se connecter au réseau"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Ressources de connectivité système"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Connectez-vous au réseau Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Se connecter au réseau"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Aucune connexion à Internet pour <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Appuyez ici pour afficher des options."</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Le réseau mobile ne dispose d\'aucun accès à Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Le réseau ne dispose d\'aucun accès à Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Impossible d\'accéder au serveur DNS privé"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"La connectivité de <xliff:g id="NETWORK_SSID">%1$s</xliff:g> est limitée"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Appuyer pour se connecter quand même"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Nouveau réseau : <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"L\'appareil utilise <xliff:g id="NEW_NETWORK">%1$s</xliff:g> lorsque <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> n\'a pas de connexion Internet. Des frais peuvent s\'appliquer."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Ancien réseau : <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>. Nouveau réseau : <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Aucune connexion à Internet pour <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Appuyez ici pour afficher des options."</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Le réseau mobile ne dispose d\'aucun accès à Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Le réseau ne dispose d\'aucun accès à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Impossible d\'accéder au serveur DNS privé"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"La connectivité de <xliff:g id="NETWORK_SSID">%1$s</xliff:g> est limitée"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Appuyer pour se connecter quand même"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Nouveau réseau : <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"L\'appareil utilise <xliff:g id="NEW_NETWORK">%1$s</xliff:g> lorsque <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> n\'a pas de connexion Internet. Des frais peuvent s\'appliquer."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Ancien réseau : <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>. Nouveau réseau : <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"données mobiles"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"données mobiles"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"type de réseau inconnu"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"type de réseau inconnu"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-gl/strings.xml b/service/ServiceConnectivityResources/res/values-gl/strings.xml
index dfe8137..f46f84b 100644
--- a/service/ServiceConnectivityResources/res/values-gl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-gl/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividade do sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Inicia sesión na rede wifi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Inicia sesión na rede"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Recursos de conectividade do sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Inicia sesión na rede wifi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Inicia sesión na rede"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ten acceso a Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toca para ver opcións."</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"A rede de telefonía móbil non ten acceso a Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"A rede non ten acceso a Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Non se puido acceder ao servidor DNS privado"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"A conectividade de <xliff:g id="NETWORK_SSID">%1$s</xliff:g> é limitada"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toca para conectarte de todas formas"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Cambiouse a: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"O dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> non ten acceso a Internet. Pódense aplicar cargos."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Cambiouse de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ten acceso a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toca para ver opcións."</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"A rede de telefonía móbil non ten acceso a Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"A rede non ten acceso a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Non se puido acceder ao servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"A conectividade de <xliff:g id="NETWORK_SSID">%1$s</xliff:g> é limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Toca para conectarte de todas formas"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Cambiouse a: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"O dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> non ten acceso a Internet. Pódense aplicar cargos."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Cambiouse de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"datos móbiles"</item>
-    <item msgid="6341719431034774569">"wifi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"datos móbiles"</item>
+    <item msgid="5624324321165953608">"wifi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tipo de rede descoñecido"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"un tipo de rede descoñecido"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-gu/strings.xml b/service/ServiceConnectivityResources/res/values-gu/strings.xml
index e49b11d..ec9ecd3 100644
--- a/service/ServiceConnectivityResources/res/values-gu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-gu/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"સિસ્ટમની કનેક્ટિવિટીનાં સાધનો"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"વાઇ-ફાઇ નેટવર્ક પર સાઇન ઇન કરો"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"નેટવર્ક પર સાઇન ઇન કરો"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"સિસ્ટમની કનેક્ટિવિટીનાં સાધનો"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"વાઇ-ફાઇ નેટવર્ક પર સાઇન ઇન કરો"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"નેટવર્ક પર સાઇન ઇન કરો"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"વિકલ્પો માટે ટૅપ કરો"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"મોબાઇલ નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ખાનગી DNS સર્વર ઍક્સેસ કરી શકાતા નથી"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> મર્યાદિત કનેક્ટિવિટી ધરાવે છે"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"છતાં કનેક્ટ કરવા માટે ટૅપ કરો"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> પર સ્વિચ કર્યું"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"જ્યારે <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> પાસે કોઈ ઇન્ટરનેટ ઍક્સેસ ન હોય ત્યારે ઉપકરણ <xliff:g id="NEW_NETWORK">%1$s</xliff:g>નો ઉપયોગ કરે છે. શુલ્ક લાગુ થઈ શકે છે."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> પરથી <xliff:g id="NEW_NETWORK">%2$s</xliff:g> પર સ્વિચ કર્યું"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"વિકલ્પો માટે ટૅપ કરો"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"મોબાઇલ નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"ખાનગી DNS સર્વર ઍક્સેસ કરી શકાતા નથી"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> મર્યાદિત કનેક્ટિવિટી ધરાવે છે"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"છતાં કનેક્ટ કરવા માટે ટૅપ કરો"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> પર સ્વિચ કર્યું"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"જ્યારે <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> પાસે કોઈ ઇન્ટરનેટ ઍક્સેસ ન હોય ત્યારે ઉપકરણ <xliff:g id="NEW_NETWORK">%1$s</xliff:g>નો ઉપયોગ કરે છે. શુલ્ક લાગુ થઈ શકે છે."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> પરથી <xliff:g id="NEW_NETWORK">%2$s</xliff:g> પર સ્વિચ કર્યું"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"મોબાઇલ ડેટા"</item>
-    <item msgid="6341719431034774569">"વાઇ-ફાઇ"</item>
-    <item msgid="5081440868800877512">"બ્લૂટૂથ"</item>
-    <item msgid="1160736166977503463">"ઇથરનેટ"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"મોબાઇલ ડેટા"</item>
+    <item msgid="5624324321165953608">"વાઇ-ફાઇ"</item>
+    <item msgid="5667906231066981731">"બ્લૂટૂથ"</item>
+    <item msgid="346574747471703768">"ઇથરનેટ"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"કોઈ અજાણ્યો નેટવર્કનો પ્રકાર"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"કોઈ અજાણ્યો નેટવર્કનો પ્રકાર"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-hi/strings.xml b/service/ServiceConnectivityResources/res/values-hi/strings.xml
index 80ed699..6e3bc6b 100644
--- a/service/ServiceConnectivityResources/res/values-hi/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-hi/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"सिस्टम कनेक्टिविटी के संसाधन"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"वाई-फ़ाई  नेटवर्क में साइन इन करें"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"नेटवर्क में साइन इन करें"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"सिस्टम कनेक्टिविटी के संसाधन"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"वाई-फ़ाई  नेटवर्क में साइन इन करें"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"नेटवर्क में साइन इन करें"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> का इंटरनेट नहीं चल रहा है"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"विकल्पों के लिए टैप करें"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"मोबाइल नेटवर्क पर इंटरनेट ऐक्सेस नहीं है"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"इस नेटवर्क पर इंटरनेट ऐक्सेस नहीं है"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"निजी डीएनएस सर्वर को ऐक्सेस नहीं किया जा सकता"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> की कनेक्टिविटी सीमित है"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"फिर भी कनेक्ट करने के लिए टैप करें"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> पर ले जाया गया"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> में इंटरनेट की सुविधा नहीं होने पर डिवाइस <xliff:g id="NEW_NETWORK">%1$s</xliff:g> का इस्तेमाल करता है. इसके लिए शुल्क लिया जा सकता है."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> से <xliff:g id="NEW_NETWORK">%2$s</xliff:g> पर ले जाया गया"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> का इंटरनेट नहीं चल रहा है"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"विकल्पों के लिए टैप करें"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"मोबाइल नेटवर्क पर इंटरनेट ऐक्सेस नहीं है"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"इस नेटवर्क पर इंटरनेट ऐक्सेस नहीं है"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"निजी डीएनएस सर्वर को ऐक्सेस नहीं किया जा सकता"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> की कनेक्टिविटी सीमित है"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"फिर भी कनेक्ट करने के लिए टैप करें"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> पर ले जाया गया"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> में इंटरनेट की सुविधा नहीं होने पर डिवाइस <xliff:g id="NEW_NETWORK">%1$s</xliff:g> का इस्तेमाल करता है. इसके लिए शुल्क लिया जा सकता है."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> से <xliff:g id="NEW_NETWORK">%2$s</xliff:g> पर ले जाया गया"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"मोबाइल डेटा"</item>
-    <item msgid="6341719431034774569">"वाई-फ़ाई"</item>
-    <item msgid="5081440868800877512">"ब्लूटूथ"</item>
-    <item msgid="1160736166977503463">"ईथरनेट"</item>
-    <item msgid="7347618872551558605">"वीपीएन"</item>
+    <item msgid="3004933964374161223">"मोबाइल डेटा"</item>
+    <item msgid="5624324321165953608">"वाई-फ़ाई"</item>
+    <item msgid="5667906231066981731">"ब्लूटूथ"</item>
+    <item msgid="346574747471703768">"ईथरनेट"</item>
+    <item msgid="5734728378097476003">"वीपीएन"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"अज्ञात नेटवर्क टाइप"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"अज्ञात नेटवर्क टाइप"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-hr/strings.xml b/service/ServiceConnectivityResources/res/values-hr/strings.xml
index 24bb22f..6a6de4c 100644
--- a/service/ServiceConnectivityResources/res/values-hr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-hr/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resursi za povezivost sustava"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prijava na Wi-Fi mrežu"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Prijava na mrežu"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Resursi za povezivost sustava"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Prijava na Wi-Fi mrežu"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Prijava na mrežu"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Dodirnite za opcije"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilna mreža nema pristup internetu"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Mreža nema pristup internetu"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Nije moguće pristupiti privatnom DNS poslužitelju"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu povezivost"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Dodirnite da biste se ipak povezali"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Prelazak na drugu mrežu: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Kada <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu, na uređaju se upotrebljava <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Moguća je naplata naknade."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Mreža je promijenjena: <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> &gt; <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Dodirnite za opcije"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilna mreža nema pristup internetu"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Mreža nema pristup internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Nije moguće pristupiti privatnom DNS poslužitelju"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu povezivost"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Dodirnite da biste se ipak povezali"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Prelazak na drugu mrežu: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Kada <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu, na uređaju se upotrebljava <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Moguća je naplata naknade."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Mreža je promijenjena: <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> &gt; <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobilni podaci"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobilni podaci"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nepoznata vrsta mreže"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"nepoznata vrsta mreže"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-hu/strings.xml b/service/ServiceConnectivityResources/res/values-hu/strings.xml
index 47a1142..1d39d30 100644
--- a/service/ServiceConnectivityResources/res/values-hu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-hu/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Rendszerkapcsolat erőforrásai"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Bejelentkezés Wi-Fi hálózatba"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Bejelentkezés a hálózatba"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Rendszerkapcsolat erőforrásai"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Bejelentkezés Wi-Fi hálózatba"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Bejelentkezés a hálózatba"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"A(z) <xliff:g id="NETWORK_SSID">%1$s</xliff:g> hálózaton nincs internet-hozzáférés"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Koppintson a beállítások megjelenítéséhez"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"A mobilhálózaton nincs internet-hozzáférés"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"A hálózaton nincs internet-hozzáférés"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"A privát DNS-kiszolgálóhoz nem lehet hozzáférni"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"A(z) <xliff:g id="NETWORK_SSID">%1$s</xliff:g> hálózat korlátozott kapcsolatot biztosít"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Koppintson, ha mindenképpen csatlakozni szeretne"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Átváltva erre: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> használata, ha nincs internet-hozzáférés <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>-kapcsolaton keresztül. A szolgáltató díjat számíthat fel."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Átváltva <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>-hálózatról erre: <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"A(z) <xliff:g id="NETWORK_SSID">%1$s</xliff:g> hálózaton nincs internet-hozzáférés"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Koppintson a beállítások megjelenítéséhez"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"A mobilhálózaton nincs internet-hozzáférés"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"A hálózaton nincs internet-hozzáférés"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"A privát DNS-kiszolgálóhoz nem lehet hozzáférni"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"A(z) <xliff:g id="NETWORK_SSID">%1$s</xliff:g> hálózat korlátozott kapcsolatot biztosít"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Koppintson, ha mindenképpen csatlakozni szeretne"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Átváltva erre: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> használata, ha nincs internet-hozzáférés <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>-kapcsolaton keresztül. A szolgáltató díjat számíthat fel."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Átváltva <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>-hálózatról erre: <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobiladatok"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobiladatok"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ismeretlen hálózati típus"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ismeretlen hálózati típus"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-hy/strings.xml b/service/ServiceConnectivityResources/res/values-hy/strings.xml
index dd951e8..99386d4 100644
--- a/service/ServiceConnectivityResources/res/values-hy/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-hy/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System Connectivity Resources"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Մուտք գործեք Wi-Fi ցանց"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Մուտք գործեք ցանց"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"System Connectivity Resources"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Մուտք գործեք Wi-Fi ցանց"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Մուտք գործեք ցանց"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ցանցը չունի մուտք ինտերնետին"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Հպեք՝ ընտրանքները տեսնելու համար"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Բջջային ցանցը չի ապահովում ինտերնետ կապ"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Ցանցը միացված չէ ինտերնետին"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Մասնավոր DNS սերվերն անհասանելի է"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ցանցի կապը սահմանափակ է"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Հպեք՝ միանալու համար"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Անցել է <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ցանցի"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Երբ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ցանցում ինտերնետ կապ չի լինում, սարքն անցնում է <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ցանցի: Նման դեպքում կարող են վճարներ գանձվել:"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ցանցից անցել է <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ցանցի"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ցանցը չունի մուտք ինտերնետին"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Հպեք՝ ընտրանքները տեսնելու համար"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Բջջային ցանցը չի ապահովում ինտերնետ կապ"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Ցանցը միացված չէ ինտերնետին"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Մասնավոր DNS սերվերն անհասանելի է"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ցանցի կապը սահմանափակ է"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Հպեք՝ միանալու համար"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Անցել է <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ցանցի"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Երբ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ցանցում ինտերնետ կապ չի լինում, սարքն անցնում է <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ցանցի: Նման դեպքում կարող են վճարներ գանձվել:"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ցանցից անցել է <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ցանցի"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"բջջային ինտերնետ"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"բջջային ինտերնետ"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ցանցի անհայտ տեսակ"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ցանցի անհայտ տեսակ"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-in/strings.xml b/service/ServiceConnectivityResources/res/values-in/strings.xml
index d559f6b..f47d257 100644
--- a/service/ServiceConnectivityResources/res/values-in/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-in/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resource Konektivitas Sistem"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Login ke jaringan Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Login ke jaringan"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Resource Konektivitas Sistem"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Login ke jaringan Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Login ke jaringan"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tidak memiliki akses internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Ketuk untuk melihat opsi"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Jaringan seluler tidak memiliki akses internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Jaringan tidak memiliki akses internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Server DNS pribadi tidak dapat diakses"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> memiliki konektivitas terbatas"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ketuk untuk tetap menyambungkan"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Dialihkan ke <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Perangkat menggunakan <xliff:g id="NEW_NETWORK">%1$s</xliff:g> jika <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tidak memiliki akses internet. Tarif mungkin berlaku."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Dialihkan dari <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ke <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tidak memiliki akses internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Ketuk untuk melihat opsi"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Jaringan seluler tidak memiliki akses internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Jaringan tidak memiliki akses internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Server DNS pribadi tidak dapat diakses"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> memiliki konektivitas terbatas"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Ketuk untuk tetap menyambungkan"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Dialihkan ke <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Perangkat menggunakan <xliff:g id="NEW_NETWORK">%1$s</xliff:g> jika <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tidak memiliki akses internet. Tarif mungkin berlaku."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Dialihkan dari <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ke <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"data seluler"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"data seluler"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"jenis jaringan yang tidak dikenal"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"jenis jaringan yang tidak dikenal"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-is/strings.xml b/service/ServiceConnectivityResources/res/values-is/strings.xml
index 877c85f..eeba231 100644
--- a/service/ServiceConnectivityResources/res/values-is/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-is/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Tengigögn kerfis"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Skrá inn á Wi-Fi net"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Skrá inn á net"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Tengigögn kerfis"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Skrá inn á Wi-Fi net"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Skrá inn á net"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> er ekki með internetaðgang"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Ýttu til að sjá valkosti"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Farsímakerfið er ekki tengt við internetið"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Netkerfið er ekki tengt við internetið"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ekki næst í DNS-einkaþjón"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Tengigeta <xliff:g id="NETWORK_SSID">%1$s</xliff:g> er takmörkuð"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ýttu til að tengjast samt"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Skipt yfir á <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Tækið notar <xliff:g id="NEW_NETWORK">%1$s</xliff:g> þegar <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> er ekki með internetaðgang. Gjöld kunna að eiga við."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Skipt úr <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> yfir í <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> er ekki með internetaðgang"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Ýttu til að sjá valkosti"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Farsímakerfið er ekki tengt við internetið"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Netkerfið er ekki tengt við internetið"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Ekki næst í DNS-einkaþjón"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Tengigeta <xliff:g id="NETWORK_SSID">%1$s</xliff:g> er takmörkuð"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Ýttu til að tengjast samt"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Skipt yfir á <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Tækið notar <xliff:g id="NEW_NETWORK">%1$s</xliff:g> þegar <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> er ekki með internetaðgang. Gjöld kunna að eiga við."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Skipt úr <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> yfir í <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"farsímagögn"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"farsímagögn"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"óþekkt tegund netkerfis"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"óþekkt tegund netkerfis"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-it/strings.xml b/service/ServiceConnectivityResources/res/values-it/strings.xml
index bcac393..ec3ff8c 100644
--- a/service/ServiceConnectivityResources/res/values-it/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-it/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Risorse per connettività di sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Accedi a rete Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Accedi alla rete"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Risorse per connettività di sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Accedi a rete Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Accedi alla rete"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ha accesso a Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tocca per le opzioni"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"La rete mobile non ha accesso a Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"La rete non ha accesso a Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Non è possibile accedere al server DNS privato"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ha una connettività limitata"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tocca per connettere comunque"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Passato a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Il dispositivo utilizza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> non ha accesso a Internet. Potrebbero essere applicati costi."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Passato da <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ha accesso a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tocca per le opzioni"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"La rete mobile non ha accesso a Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"La rete non ha accesso a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Non è possibile accedere al server DNS privato"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ha una connettività limitata"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tocca per connettere comunque"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Passato a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Il dispositivo utilizza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> non ha accesso a Internet. Potrebbero essere applicati costi."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Passato da <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"dati mobili"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"dati mobili"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"tipo di rete sconosciuto"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"tipo di rete sconosciuto"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-iw/strings.xml b/service/ServiceConnectivityResources/res/values-iw/strings.xml
index d6684ce..d123ebb 100644
--- a/service/ServiceConnectivityResources/res/values-iw/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-iw/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"משאבי קישוריות מערכת"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"‏היכנס לרשת Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"היכנס לרשת"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"משאבי קישוריות מערכת"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"‏היכנס לרשת Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"היכנס לרשת"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"ל-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> אין גישה לאינטרנט"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"הקש לקבלת אפשרויות"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"לרשת הסלולרית אין גישה לאינטרנט"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"לרשת אין גישה לאינטרנט"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"‏לא ניתן לגשת לשרת DNS הפרטי"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"הקישוריות של <xliff:g id="NETWORK_SSID">%1$s</xliff:g> מוגבלת"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"כדי להתחבר למרות זאת יש להקיש"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"מעבר אל <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"המכשיר משתמש ברשת <xliff:g id="NEW_NETWORK">%1$s</xliff:g> כשלרשת <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> אין גישה לאינטרנט. עשויים לחול חיובים."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"עבר מרשת <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> לרשת <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"ל-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> אין גישה לאינטרנט"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"הקש לקבלת אפשרויות"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"לרשת הסלולרית אין גישה לאינטרנט"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"לרשת אין גישה לאינטרנט"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"‏לא ניתן לגשת לשרת DNS הפרטי"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"הקישוריות של <xliff:g id="NETWORK_SSID">%1$s</xliff:g> מוגבלת"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"כדי להתחבר למרות זאת יש להקיש"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"מעבר אל <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"המכשיר משתמש ברשת <xliff:g id="NEW_NETWORK">%1$s</xliff:g> כשלרשת <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> אין גישה לאינטרנט. עשויים לחול חיובים."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"עבר מרשת <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> לרשת <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"חבילת גלישה"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"אתרנט"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"חבילת גלישה"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"אתרנט"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"סוג רשת לא מזוהה"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"סוג רשת לא מזוהה"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ja/strings.xml b/service/ServiceConnectivityResources/res/values-ja/strings.xml
index fa4a30a..7bb6f85 100644
--- a/service/ServiceConnectivityResources/res/values-ja/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ja/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"システム接続リソース"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fiネットワークにログイン"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ネットワークにログインしてください"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"システム接続リソース"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fiネットワークにログイン"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ネットワークにログインしてください"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> はインターネットにアクセスできません"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"タップしてその他のオプションを表示"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"モバイル ネットワークがインターネットに接続されていません"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"ネットワークがインターネットに接続されていません"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"プライベート DNS サーバーにアクセスできません"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> の接続が制限されています"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"接続するにはタップしてください"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"「<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>」に切り替えました"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"デバイスで「<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>」によるインターネット接続ができない場合に「<xliff:g id="NEW_NETWORK">%1$s</xliff:g>」を使用します。通信料が発生することがあります。"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"「<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>」から「<xliff:g id="NEW_NETWORK">%2$s</xliff:g>」に切り替えました"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> はインターネットにアクセスできません"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"タップしてその他のオプションを表示"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"モバイル ネットワークがインターネットに接続されていません"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"ネットワークがインターネットに接続されていません"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"プライベート DNS サーバーにアクセスできません"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> の接続が制限されています"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"接続するにはタップしてください"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"「<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>」に切り替えました"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"デバイスで「<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>」によるインターネット接続ができない場合に「<xliff:g id="NEW_NETWORK">%1$s</xliff:g>」を使用します。通信料が発生することがあります。"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"「<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>」から「<xliff:g id="NEW_NETWORK">%2$s</xliff:g>」に切り替えました"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"モバイルデータ"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"イーサネット"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"モバイルデータ"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"イーサネット"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"不明なネットワーク タイプ"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"不明なネットワーク タイプ"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ka/strings.xml b/service/ServiceConnectivityResources/res/values-ka/strings.xml
index 4183310..f42c567 100644
--- a/service/ServiceConnectivityResources/res/values-ka/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ka/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"სისტემის კავშირის რესურსები"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi ქსელთან დაკავშირება"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ქსელში შესვლა"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"სისტემის კავშირის რესურსები"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi ქსელთან დაკავშირება"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ქსელში შესვლა"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ს არ აქვს ინტერნეტზე წვდომა"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"შეეხეთ ვარიანტების სანახავად"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"მობილურ ქსელს არ აქვს ინტერნეტზე წვდომა"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"ქსელს არ აქვს ინტერნეტზე წვდომა"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"პირად DNS სერვერზე წვდომა შეუძლებელია"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ის კავშირები შეზღუდულია"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"შეეხეთ, თუ მაინც გსურთ დაკავშირება"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"ახლა გამოიყენება <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"თუ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ინტერნეტთან კავშირს დაკარგავს, მოწყობილობის მიერ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> იქნება გამოყენებული, რამაც შეიძლება დამატებითი ხარჯები გამოიწვიოს."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"ახლა გამოიყენება <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> (გამოიყენებოდა <xliff:g id="NEW_NETWORK">%2$s</xliff:g>)"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ს არ აქვს ინტერნეტზე წვდომა"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"შეეხეთ ვარიანტების სანახავად"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"მობილურ ქსელს არ აქვს ინტერნეტზე წვდომა"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"ქსელს არ აქვს ინტერნეტზე წვდომა"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"პირად DNS სერვერზე წვდომა შეუძლებელია"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ის კავშირები შეზღუდულია"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"შეეხეთ, თუ მაინც გსურთ დაკავშირება"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"ახლა გამოიყენება <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"თუ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ინტერნეტთან კავშირს დაკარგავს, მოწყობილობის მიერ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> იქნება გამოყენებული, რამაც შეიძლება დამატებითი ხარჯები გამოიწვიოს."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"ახლა გამოიყენება <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> (გამოიყენებოდა <xliff:g id="NEW_NETWORK">%2$s</xliff:g>)"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"მობილური ინტერნეტი"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"მობილური ინტერნეტი"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"უცნობი ტიპის ქსელი"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"უცნობი ტიპის ქსელი"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-kk/strings.xml b/service/ServiceConnectivityResources/res/values-kk/strings.xml
index 54d5eb3..00c0f39 100644
--- a/service/ServiceConnectivityResources/res/values-kk/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-kk/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Жүйе байланысы ресурстары"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi желісіне кіру"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Желіге кіру"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Жүйе байланысы ресурстары"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi желісіне кіру"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Желіге кіру"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желісінің интернетті пайдалану мүмкіндігі шектеулі."</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Опциялар үшін түртіңіз"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мобильдік желі интернетке қосылмаған."</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Желі интернетке қосылмаған."</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Жеке DNS серверіне кіру мүмкін емес."</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желісінің қосылу мүмкіндігі шектеулі."</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Бәрібір жалғау үшін түртіңіз."</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> желісіне ауысты"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Құрылғы <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> желісінде интернетпен байланыс жоғалған жағдайда <xliff:g id="NEW_NETWORK">%1$s</xliff:g> желісін пайдаланады. Деректер ақысы алынуы мүмкін."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> желісінен <xliff:g id="NEW_NETWORK">%2$s</xliff:g> желісіне ауысты"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желісінің интернетті пайдалану мүмкіндігі шектеулі."</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Опциялар үшін түртіңіз"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобильдік желі интернетке қосылмаған."</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Желі интернетке қосылмаған."</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Жеке DNS серверіне кіру мүмкін емес."</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желісінің қосылу мүмкіндігі шектеулі."</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Бәрібір жалғау үшін түртіңіз."</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> желісіне ауысты"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Құрылғы <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> желісінде интернетпен байланыс жоғалған жағдайда <xliff:g id="NEW_NETWORK">%1$s</xliff:g> желісін пайдаланады. Деректер ақысы алынуы мүмкін."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> желісінен <xliff:g id="NEW_NETWORK">%2$s</xliff:g> желісіне ауысты"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мобильдік деректер"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мобильдік деректер"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"желі түрі белгісіз"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"желі түрі белгісіз"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-km/strings.xml b/service/ServiceConnectivityResources/res/values-km/strings.xml
index bd778a1..fa06c5b 100644
--- a/service/ServiceConnectivityResources/res/values-km/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-km/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ធនធាន​តភ្ជាប់​ប្រព័ន្ធ"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ចូល​បណ្ដាញ​វ៉ាយហ្វាយ"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ចូលទៅបណ្តាញ"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ធនធាន​តភ្ជាប់​ប្រព័ន្ធ"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ចូល​បណ្ដាញ​វ៉ាយហ្វាយ"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ចូលទៅបណ្តាញ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មិនមាន​ការតភ្ជាប់អ៊ីនធឺណិត​ទេ"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ប៉ះសម្រាប់ជម្រើស"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"បណ្ដាញ​ទូរសព្ទ​ចល័ត​មិនមានការតភ្ជាប់​អ៊ីនធឺណិតទេ"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"បណ្ដាញ​មិនមាន​ការតភ្ជាប់​អ៊ីនធឺណិតទេ"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"មិនអាច​ចូលប្រើ​ម៉ាស៊ីនមេ DNS ឯកជន​បានទេ"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មានការតភ្ជាប់​មានកម្រិត"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"មិន​អី​ទេ ចុច​​ភ្ជាប់​ចុះ"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"បានប្តូរទៅ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"ឧបករណ៍​ប្រើ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> នៅ​ពេល​ដែល <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> មិនមាន​ការ​តភ្ជាប់​អ៊ីនធឺណិត។ អាច​គិតថ្លៃ​លើការ​ប្រើប្រាស់​ទិន្នន័យ។"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"បានប្តូរពី <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ទៅ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មិនមាន​ការតភ្ជាប់អ៊ីនធឺណិត​ទេ"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ប៉ះសម្រាប់ជម្រើស"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"បណ្ដាញ​ទូរសព្ទ​ចល័ត​មិនមានការតភ្ជាប់​អ៊ីនធឺណិតទេ"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"បណ្ដាញ​មិនមាន​ការតភ្ជាប់​អ៊ីនធឺណិតទេ"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"មិនអាច​ចូលប្រើ​ម៉ាស៊ីនមេ DNS ឯកជន​បានទេ"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មានការតភ្ជាប់​មានកម្រិត"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"មិន​អី​ទេ ចុច​​ភ្ជាប់​ចុះ"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"បានប្តូរទៅ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"ឧបករណ៍​ប្រើ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> នៅ​ពេល​ដែល <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> មិនមាន​ការ​តភ្ជាប់​អ៊ីនធឺណិត។ អាច​គិតថ្លៃ​លើការ​ប្រើប្រាស់​ទិន្នន័យ។"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"បានប្តូរពី <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ទៅ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"ទិន្នន័យ​ទូរសព្ទចល័ត"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"ប៊្លូធូស"</item>
-    <item msgid="1160736166977503463">"អ៊ីសឺរណិត"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"ទិន្នន័យ​ទូរសព្ទចល័ត"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"ប៊្លូធូស"</item>
+    <item msgid="346574747471703768">"អ៊ីសឺរណិត"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ប្រភេទបណ្តាញដែលមិនស្គាល់"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ប្រភេទបណ្តាញដែលមិនស្គាល់"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-kn/strings.xml b/service/ServiceConnectivityResources/res/values-kn/strings.xml
index 7f3a420..cde8fac 100644
--- a/service/ServiceConnectivityResources/res/values-kn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-kn/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ಸಿಸ್ಟಂ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆ ಮಾಹಿತಿಯ ಮೂಲಗಳು"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ವೈ-ಫೈ ನೆಟ್‍ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ಸಿಸ್ಟಂ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆ ಮಾಹಿತಿಯ ಮೂಲಗಳು"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ವೈ-ಫೈ ನೆಟ್‍ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ಆಯ್ಕೆಗಳಿಗೆ ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"ಮೊಬೈಲ್ ನೆಟ್‌ವರ್ಕ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"ನೆಟ್‌ವರ್ಕ್‌ ಇಂಟರ್ನೆಟ್‌ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ಖಾಸಗಿ DNS ಸರ್ವರ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಸೀಮಿತ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆಯನ್ನು ಹೊಂದಿದೆ"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ಹೇಗಾದರೂ ಸಂಪರ್ಕಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶ ಹೊಂದಿಲ್ಲದಿರುವಾಗ, ಸಾಧನವು <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ಬಳಸುತ್ತದೆ. ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ರಿಂದ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ಆಯ್ಕೆಗಳಿಗೆ ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"ಮೊಬೈಲ್ ನೆಟ್‌ವರ್ಕ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"ನೆಟ್‌ವರ್ಕ್‌ ಇಂಟರ್ನೆಟ್‌ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"ಖಾಸಗಿ DNS ಸರ್ವರ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಸೀಮಿತ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆಯನ್ನು ಹೊಂದಿದೆ"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"ಹೇಗಾದರೂ ಸಂಪರ್ಕಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶ ಹೊಂದಿಲ್ಲದಿರುವಾಗ, ಸಾಧನವು <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ಬಳಸುತ್ತದೆ. ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ರಿಂದ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"ಮೊಬೈಲ್ ಡೇಟಾ"</item>
-    <item msgid="6341719431034774569">"ವೈ-ಫೈ"</item>
-    <item msgid="5081440868800877512">"ಬ್ಲೂಟೂತ್"</item>
-    <item msgid="1160736166977503463">"ಇಥರ್ನೆಟ್"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"ಮೊಬೈಲ್ ಡೇಟಾ"</item>
+    <item msgid="5624324321165953608">"ವೈ-ಫೈ"</item>
+    <item msgid="5667906231066981731">"ಬ್ಲೂಟೂತ್"</item>
+    <item msgid="346574747471703768">"ಇಥರ್ನೆಟ್"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ಅಪರಿಚಿತ ನೆಟ್‌ವರ್ಕ್ ಪ್ರಕಾರ"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ಅಪರಿಚಿತ ನೆಟ್‌ವರ್ಕ್ ಪ್ರಕಾರ"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ko/strings.xml b/service/ServiceConnectivityResources/res/values-ko/strings.xml
index a763cc5..eef59a9 100644
--- a/service/ServiceConnectivityResources/res/values-ko/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ko/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"시스템 연결 리소스"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi 네트워크에 로그인"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"네트워크에 로그인"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"시스템 연결 리소스"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi 네트워크에 로그인"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"네트워크에 로그인"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>이(가) 인터넷에 액세스할 수 없습니다."</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"탭하여 옵션 보기"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"모바일 네트워크에 인터넷이 연결되어 있지 않습니다."</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"네트워크에 인터넷이 연결되어 있지 않습니다."</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"비공개 DNS 서버에 액세스할 수 없습니다."</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>에서 연결을 제한했습니다."</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"계속 연결하려면 탭하세요."</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>(으)로 전환"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>(으)로 인터넷에 연결할 수 없는 경우 기기에서 <xliff:g id="NEW_NETWORK">%1$s</xliff:g>이(가) 사용됩니다. 요금이 부과될 수 있습니다."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>에서 <xliff:g id="NEW_NETWORK">%2$s</xliff:g>(으)로 전환"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>이(가) 인터넷에 액세스할 수 없습니다."</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"탭하여 옵션 보기"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"모바일 네트워크에 인터넷이 연결되어 있지 않습니다."</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"네트워크에 인터넷이 연결되어 있지 않습니다."</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"비공개 DNS 서버에 액세스할 수 없습니다."</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>에서 연결을 제한했습니다."</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"계속 연결하려면 탭하세요."</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>(으)로 전환"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>(으)로 인터넷에 연결할 수 없는 경우 기기에서 <xliff:g id="NEW_NETWORK">%1$s</xliff:g>이(가) 사용됩니다. 요금이 부과될 수 있습니다."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>에서 <xliff:g id="NEW_NETWORK">%2$s</xliff:g>(으)로 전환"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"모바일 데이터"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"블루투스"</item>
-    <item msgid="1160736166977503463">"이더넷"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"모바일 데이터"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"블루투스"</item>
+    <item msgid="346574747471703768">"이더넷"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"알 수 없는 네트워크 유형"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"알 수 없는 네트워크 유형"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ky/strings.xml b/service/ServiceConnectivityResources/res/values-ky/strings.xml
index 3550af8..0027c8a 100644
--- a/service/ServiceConnectivityResources/res/values-ky/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ky/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Тутумдун байланыш булагы"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi түйүнүнө кирүү"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Тармакка кирүү"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Системанын байланыш булагы"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi түйүнүнө кирүү"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Тармакка кирүү"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> Интернетке туташуусу жок"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Параметрлерди ачуу үчүн таптап коюңуз"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мобилдик Интернет жок"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Тармактын Интернет жок"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Жеке DNS сервери жеткиликсиз"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> байланышы чектелген"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Баары бир туташуу үчүн таптаңыз"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> тармагына которуштурулду"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> тармагы Интернетке туташпай турганда, түзмөгүңүз <xliff:g id="NEW_NETWORK">%1$s</xliff:g> тармагын колдонот. Акы алынышы мүмкүн."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> дегенден <xliff:g id="NEW_NETWORK">%2$s</xliff:g> тармагына которуштурулду"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> Интернетке туташуусу жок"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Параметрлерди ачуу үчүн таптап коюңуз"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобилдик Интернет жок"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Тармактын Интернет жок"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Жеке DNS сервери жеткиликсиз"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> байланышы чектелген"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Баары бир туташуу үчүн таптаңыз"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> тармагына которуштурулду"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> тармагы Интернетке туташпай турганда, түзмөгүңүз <xliff:g id="NEW_NETWORK">%1$s</xliff:g> тармагын колдонот. Акы алынышы мүмкүн."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> дегенден <xliff:g id="NEW_NETWORK">%2$s</xliff:g> тармагына которуштурулду"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мобилдик трафик"</item>
-    <item msgid="6341719431034774569">"Wi‑Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мобилдик трафик"</item>
+    <item msgid="5624324321165953608">"Wi‑Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"белгисиз тармак түрү"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"белгисиз тармак түрү"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-lo/strings.xml b/service/ServiceConnectivityResources/res/values-lo/strings.xml
index 4b3056f..64419f9 100644
--- a/service/ServiceConnectivityResources/res/values-lo/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-lo/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ແຫຼ່ງຂໍ້ມູນການເຊື່ອມຕໍ່ລະບົບ"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ເຂົ້າສູ່ລະບົບເຄືອຂ່າຍ Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ລົງຊື່ເຂົ້າເຄືອຂ່າຍ"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ແຫຼ່ງຂໍ້ມູນການເຊື່ອມຕໍ່ລະບົບ"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ເຂົ້າສູ່ລະບົບເຄືອຂ່າຍ Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ລົງຊື່ເຂົ້າເຄືອຂ່າຍ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ແຕະເພື່ອເບິ່ງຕົວເລືອກ"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"ເຄືອຂ່າຍມືຖືບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"ເຄືອຂ່າຍບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ບໍ່ສາມາດເຂົ້າເຖິງເຊີບເວີ DNS ສ່ວນຕົວໄດ້"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ມີການເຊື່ອມຕໍ່ທີ່ຈຳກັດ"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ແຕະເພື່ອຢືນຢັນການເຊື່ອມຕໍ່"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"ສະຫຼັບໄປໃຊ້ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ແລ້ວ"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"ອຸປະກອນຈະໃຊ້ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ເມື່ອ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ. ອາດມີການຮຽກເກັບຄ່າບໍລິການ."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"ສະຫຼັບຈາກ <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ໄປໃຊ້ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ແລ້ວ"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ແຕະເພື່ອເບິ່ງຕົວເລືອກ"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"ເຄືອຂ່າຍມືຖືບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"ເຄືອຂ່າຍບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"ບໍ່ສາມາດເຂົ້າເຖິງເຊີບເວີ DNS ສ່ວນຕົວໄດ້"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ມີການເຊື່ອມຕໍ່ທີ່ຈຳກັດ"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"ແຕະເພື່ອຢືນຢັນການເຊື່ອມຕໍ່"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"ສະຫຼັບໄປໃຊ້ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ແລ້ວ"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"ອຸປະກອນຈະໃຊ້ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ເມື່ອ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ. ອາດມີການຮຽກເກັບຄ່າບໍລິການ."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"ສະຫຼັບຈາກ <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ໄປໃຊ້ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ແລ້ວ"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"ອິນເຕີເນັດມືຖື"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"ອີເທີເນັດ"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"ອິນເຕີເນັດມືຖື"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"ອີເທີເນັດ"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ບໍ່ຮູ້ຈັກປະເພດເຄືອຂ່າຍ"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ບໍ່ຮູ້ຈັກປະເພດເຄືອຂ່າຍ"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-lt/strings.xml b/service/ServiceConnectivityResources/res/values-lt/strings.xml
index 8eb41f1..f73f142 100644
--- a/service/ServiceConnectivityResources/res/values-lt/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-lt/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System Connectivity Resources"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prisijungti prie „Wi-Fi“ tinklo"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Prisijungti prie tinklo"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"System Connectivity Resources"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Prisijungti prie „Wi-Fi“ tinklo"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Prisijungti prie tinklo"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ negali pasiekti interneto"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Palieskite, kad būtų rodomos parinktys."</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiliojo ryšio tinkle nėra prieigos prie interneto"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Tinkle nėra prieigos prie interneto"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Privataus DNS serverio negalima pasiekti"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ ryšys apribotas"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Palieskite, jei vis tiek norite prisijungti"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Perjungta į tinklą <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Įrenginyje naudojamas kitas tinklas (<xliff:g id="NEW_NETWORK">%1$s</xliff:g>), kai dabartiniame tinkle (<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>) nėra interneto ryšio. Gali būti taikomi mokesčiai."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Perjungta iš tinklo <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> į tinklą <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ negali pasiekti interneto"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Palieskite, kad būtų rodomos parinktys."</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiliojo ryšio tinkle nėra prieigos prie interneto"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Tinkle nėra prieigos prie interneto"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Privataus DNS serverio negalima pasiekti"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ ryšys apribotas"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Palieskite, jei vis tiek norite prisijungti"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Perjungta į tinklą <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Įrenginyje naudojamas kitas tinklas (<xliff:g id="NEW_NETWORK">%1$s</xliff:g>), kai dabartiniame tinkle (<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>) nėra interneto ryšio. Gali būti taikomi mokesčiai."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Perjungta iš tinklo <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> į tinklą <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobiliojo ryšio duomenys"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Eternetas"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobiliojo ryšio duomenys"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Eternetas"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nežinomas tinklo tipas"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"nežinomas tinklo tipas"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-lv/strings.xml b/service/ServiceConnectivityResources/res/values-lv/strings.xml
index 0647a4f..ce063a5 100644
--- a/service/ServiceConnectivityResources/res/values-lv/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-lv/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sistēmas savienojamības resursi"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Pierakstieties Wi-Fi tīklā"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Pierakstīšanās tīklā"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Sistēmas savienojamības resursi"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Pierakstieties Wi-Fi tīklā"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Pierakstīšanās tīklā"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Tīklā <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nav piekļuves internetam"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Pieskarieties, lai skatītu iespējas."</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilajā tīklā nav piekļuves internetam."</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Tīklā nav piekļuves internetam."</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Nevar piekļūt privātam DNS serverim."</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Tīklā <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ir ierobežota savienojamība"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Lai tik un tā izveidotu savienojumu, pieskarieties"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Pārslēdzās uz tīklu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Kad vienā tīklā (<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>) nav piekļuves internetam, ierīcē tiek izmantots cits tīkls (<xliff:g id="NEW_NETWORK">%1$s</xliff:g>). Var tikt piemērota maksa."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Pārslēdzās no tīkla <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> uz tīklu <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <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 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>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Tīklā <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ir ierobežota savienojamība"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Lai tik un tā izveidotu savienojumu, pieskarieties"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Pārslēdzās uz tīklu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Kad vienā tīklā (<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>) nav piekļuves internetam, ierīcē tiek izmantots cits tīkls (<xliff:g id="NEW_NETWORK">%1$s</xliff:g>). Var tikt piemērota maksa."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Pārslēdzās no tīkla <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> uz tīklu <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobilie dati"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobilie dati"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nezināms tīkla veids"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"nezināms tīkla veids"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-mk/strings.xml b/service/ServiceConnectivityResources/res/values-mk/strings.xml
index b0024e2..fb105e0 100644
--- a/service/ServiceConnectivityResources/res/values-mk/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-mk/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System Connectivity Resources"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Најавете се на мрежа на Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Најавете се на мрежа"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"System Connectivity Resources"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Најавете се на мрежа на Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Најавете се на мрежа"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема интернет-пристап"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Допрете за опции"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мобилната мрежа нема интернет-пристап"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Мрежата нема интернет-пристап"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Не може да се пристапи до приватниот DNS-сервер"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничена поврзливост"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Допрете за да се поврзете и покрај тоа"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Префрлено на <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Уредот користи <xliff:g id="NEW_NETWORK">%1$s</xliff:g> кога <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> нема пристап до интернет. Може да се наплатат трошоци."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Префрлено од <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема интернет-пристап"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Допрете за опции"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобилната мрежа нема интернет-пристап"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Мрежата нема интернет-пристап"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Не може да се пристапи до приватниот DNS-сервер"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничена поврзливост"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Допрете за да се поврзете и покрај тоа"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Префрлено на <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Уредот користи <xliff:g id="NEW_NETWORK">%1$s</xliff:g> кога <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> нема пристап до интернет. Може да се наплатат трошоци."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Префрлено од <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мобилен интернет"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Етернет"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мобилен интернет"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Етернет"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"непознат тип мрежа"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"непознат тип мрежа"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ml/strings.xml b/service/ServiceConnectivityResources/res/values-ml/strings.xml
index 8ce7667..9a51238 100644
--- a/service/ServiceConnectivityResources/res/values-ml/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ml/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"സിസ്‌റ്റം കണക്‌റ്റിവിറ്റി ഉറവിടങ്ങൾ"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"വൈഫൈ നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"സിസ്‌റ്റം കണക്‌റ്റിവിറ്റി ഉറവിടങ്ങൾ"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"വൈഫൈ നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> എന്നതിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ഓപ്ഷനുകൾക്ക് ടാപ്പുചെയ്യുക"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"മൊബെെൽ നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"സ്വകാര്യ DNS സെർവർ ആക്‌സസ് ചെയ്യാനാവില്ല"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> എന്നതിന് പരിമിതമായ കണക്റ്റിവിറ്റി ഉണ്ട്"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ഏതുവിധേനയും കണക്‌റ്റ് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> എന്നതിലേക്ക് മാറി"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>-ന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ലാത്തപ്പോൾ ഉപകരണം <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ഉപയോഗിക്കുന്നു. നിരക്കുകൾ ബാധകമായേക്കാം."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> നെറ്റ്‌വർക്കിൽ നിന്ന് <xliff:g id="NEW_NETWORK">%2$s</xliff:g> നെറ്റ്‌വർക്കിലേക്ക് മാറി"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> എന്നതിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ഓപ്ഷനുകൾക്ക് ടാപ്പുചെയ്യുക"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"മൊബെെൽ നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"സ്വകാര്യ DNS സെർവർ ആക്‌സസ് ചെയ്യാനാവില്ല"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> എന്നതിന് പരിമിതമായ കണക്റ്റിവിറ്റി ഉണ്ട്"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"ഏതുവിധേനയും കണക്‌റ്റ് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> എന്നതിലേക്ക് മാറി"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>-ന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ലാത്തപ്പോൾ ഉപകരണം <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ഉപയോഗിക്കുന്നു. നിരക്കുകൾ ബാധകമായേക്കാം."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> നെറ്റ്‌വർക്കിൽ നിന്ന് <xliff:g id="NEW_NETWORK">%2$s</xliff:g> നെറ്റ്‌വർക്കിലേക്ക് മാറി"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"മൊബൈൽ ഡാറ്റ"</item>
-    <item msgid="6341719431034774569">"വൈഫൈ"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"ഇതർനെറ്റ്"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"മൊബൈൽ ഡാറ്റ"</item>
+    <item msgid="5624324321165953608">"വൈഫൈ"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"ഇതർനെറ്റ്"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"അജ്ഞാതമായ നെറ്റ്‌വർക്ക് തരം"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"അജ്ഞാതമായ നെറ്റ്‌വർക്ക് തരം"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-mn/strings.xml b/service/ServiceConnectivityResources/res/values-mn/strings.xml
index be8b592..8372533 100644
--- a/service/ServiceConnectivityResources/res/values-mn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-mn/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Системийн холболтын нөөцүүд"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi сүлжээнд нэвтэрнэ үү"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Сүлжээнд нэвтэрнэ үү"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Системийн холболтын нөөцүүд"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi сүлжээнд нэвтэрнэ үү"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Сүлжээнд нэвтэрнэ үү"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-д интернэтийн хандалт алга"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Сонголт хийхийн тулд товшино уу"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мобайл сүлжээнд интернэт хандалт байхгүй байна"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Сүлжээнд интернэт хандалт байхгүй байна"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Хувийн DNS серверт хандах боломжгүй байна"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> зарим үйлчилгээнд хандах боломжгүй байна"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ямар ч тохиолдолд холбогдохын тулд товших"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> руу шилжүүлсэн"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> интернет холболтгүй үед төхөөрөмж <xliff:g id="NEW_NETWORK">%1$s</xliff:g>-г ашигладаг. Төлбөр гарч болзошгүй."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>-с <xliff:g id="NEW_NETWORK">%2$s</xliff:g> руу шилжүүлсэн"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-д интернэтийн хандалт алга"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Сонголт хийхийн тулд товшино уу"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобайл сүлжээнд интернэт хандалт байхгүй байна"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Сүлжээнд интернэт хандалт байхгүй байна"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Хувийн DNS серверт хандах боломжгүй байна"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> зарим үйлчилгээнд хандах боломжгүй байна"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Ямар ч тохиолдолд холбогдохын тулд товших"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> руу шилжүүлсэн"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> интернет холболтгүй үед төхөөрөмж <xliff:g id="NEW_NETWORK">%1$s</xliff:g>-г ашигладаг. Төлбөр гарч болзошгүй."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>-с <xliff:g id="NEW_NETWORK">%2$s</xliff:g> руу шилжүүлсэн"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мобайл дата"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Этернэт"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мобайл дата"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Этернэт"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"үл мэдэгдэх сүлжээний төрөл"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"үл мэдэгдэх сүлжээний төрөл"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-mr/strings.xml b/service/ServiceConnectivityResources/res/values-mr/strings.xml
index fe7df84..658b19b 100644
--- a/service/ServiceConnectivityResources/res/values-mr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-mr/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"सिस्टम कनेक्टिव्हिटी चे स्रोत"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"वाय-फाय नेटवर्कमध्‍ये साइन इन करा"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"नेटवर्कवर साइन इन करा"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"सिस्टम कनेक्टिव्हिटी चे स्रोत"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"वाय-फाय नेटवर्कमध्‍ये साइन इन करा"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"नेटवर्कवर साइन इन करा"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला इंटरनेट अ‍ॅक्सेस नाही"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"पर्यायांसाठी टॅप करा"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"मोबाइल नेटवर्कला इंटरनेट ॲक्सेस नाही"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"नेटवर्कला इंटरनेट ॲक्सेस नाही"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"खाजगी DNS सर्व्हर ॲक्सेस करू शकत नाही"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला मर्यादित कनेक्टिव्हिटी आहे"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"तरीही कनेक्ट करण्यासाठी टॅप करा"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> वर स्विच केले"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> कडे इंटरनेटचा अ‍ॅक्सेस नसताना डिव्हाइस <xliff:g id="NEW_NETWORK">%1$s</xliff:g> वापरते. शुल्क लागू शकते."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> वरून <xliff:g id="NEW_NETWORK">%2$s</xliff:g> वर स्विच केले"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला इंटरनेट अ‍ॅक्सेस नाही"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"पर्यायांसाठी टॅप करा"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"मोबाइल नेटवर्कला इंटरनेट ॲक्सेस नाही"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"नेटवर्कला इंटरनेट ॲक्सेस नाही"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"खाजगी DNS सर्व्हर ॲक्सेस करू शकत नाही"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला मर्यादित कनेक्टिव्हिटी आहे"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"तरीही कनेक्ट करण्यासाठी टॅप करा"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> वर स्विच केले"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> कडे इंटरनेटचा अ‍ॅक्सेस नसताना डिव्हाइस <xliff:g id="NEW_NETWORK">%1$s</xliff:g> वापरते. शुल्क लागू शकते."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> वरून <xliff:g id="NEW_NETWORK">%2$s</xliff:g> वर स्विच केले"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"मोबाइल डेटा"</item>
-    <item msgid="6341719431034774569">"वाय-फाय"</item>
-    <item msgid="5081440868800877512">"ब्लूटूथ"</item>
-    <item msgid="1160736166977503463">"इथरनेट"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"मोबाइल डेटा"</item>
+    <item msgid="5624324321165953608">"वाय-फाय"</item>
+    <item msgid="5667906231066981731">"ब्लूटूथ"</item>
+    <item msgid="346574747471703768">"इथरनेट"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"अज्ञात नेटवर्क प्रकार"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"अज्ञात नेटवर्क प्रकार"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ms/strings.xml b/service/ServiceConnectivityResources/res/values-ms/strings.xml
index 54b49a2..84b242c 100644
--- a/service/ServiceConnectivityResources/res/values-ms/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ms/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sumber Kesambungan Sistem"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Log masuk ke rangkaian Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Log masuk ke rangkaian"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Sumber Kesambungan Sistem"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Log masuk ke rangkaian Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Log masuk ke rangkaian"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiada akses Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Ketik untuk mendapatkan pilihan"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Rangkaian mudah alih tiada akses Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Rangkaian tiada akses Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Pelayan DNS peribadi tidak boleh diakses"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> mempunyai kesambungan terhad"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ketik untuk menyambung juga"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Beralih kepada <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Peranti menggunakan <xliff:g id="NEW_NETWORK">%1$s</xliff:g> apabila <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tiada akses Internet. Bayaran mungkin dikenakan."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Beralih daripada <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> kepada <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiada akses Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Ketik untuk mendapatkan pilihan"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Rangkaian mudah alih tiada akses Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Rangkaian tiada akses Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Pelayan DNS peribadi tidak boleh diakses"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> mempunyai kesambungan terhad"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Ketik untuk menyambung juga"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Beralih kepada <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Peranti menggunakan <xliff:g id="NEW_NETWORK">%1$s</xliff:g> apabila <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tiada akses Internet. Bayaran mungkin dikenakan."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Beralih daripada <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> kepada <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"data mudah alih"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"data mudah alih"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"jenis rangkaian tidak diketahui"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"jenis rangkaian tidak diketahui"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-my/strings.xml b/service/ServiceConnectivityResources/res/values-my/strings.xml
index 15b75f0..6832263 100644
--- a/service/ServiceConnectivityResources/res/values-my/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-my/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"စနစ်ချိတ်ဆက်နိုင်မှု ရင်းမြစ်များ"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ဝိုင်ဖိုင်ကွန်ရက်သို့ လက်မှတ်ထိုးဝင်ပါ"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ကွန်ယက်သို့ လက်မှတ်ထိုးဝင်ရန်"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"စနစ်ချိတ်ဆက်နိုင်မှု ရင်းမြစ်များ"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ဝိုင်ဖိုင်ကွန်ရက်သို့ လက်မှတ်ထိုးဝင်ပါ"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ကွန်ယက်သို့ လက်မှတ်ထိုးဝင်ရန်"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"အခြားရွေးချယ်စရာများကိုကြည့်ရန် တို့ပါ"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"မိုဘိုင်းကွန်ရက်တွင် အင်တာနက်ချိတ်ဆက်မှု မရှိပါ"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"ကွန်ရက်တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"သီးသန့် ဒီအန်အက်စ် (DNS) ဆာဗာကို သုံး၍မရပါ။"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> တွင် ချိတ်ဆက်မှုကို ကန့်သတ်ထားသည်"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"မည်သို့ပင်ဖြစ်စေ ချိတ်ဆက်ရန် တို့ပါ"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> သို့ ပြောင်းလိုက်ပြီ"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ဖြင့် အင်တာနက် အသုံးမပြုနိုင်သည့်အချိန်တွင် စက်ပစ္စည်းသည် <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ကို သုံးပါသည်။ ဒေတာသုံးစွဲခ ကျသင့်နိုင်ပါသည်။"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> မှ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> သို့ ပြောင်းလိုက်ပြီ"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"အခြားရွေးချယ်စရာများကိုကြည့်ရန် တို့ပါ"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"မိုဘိုင်းကွန်ရက်တွင် အင်တာနက်ချိတ်ဆက်မှု မရှိပါ"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"ကွန်ရက်တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"သီးသန့် ဒီအန်အက်စ် (DNS) ဆာဗာကို သုံး၍မရပါ။"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> တွင် ချိတ်ဆက်မှုကို ကန့်သတ်ထားသည်"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"မည်သို့ပင်ဖြစ်စေ ချိတ်ဆက်ရန် တို့ပါ"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> သို့ ပြောင်းလိုက်ပြီ"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ဖြင့် အင်တာနက် အသုံးမပြုနိုင်သည့်အချိန်တွင် စက်ပစ္စည်းသည် <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ကို သုံးပါသည်။ ဒေတာသုံးစွဲခ ကျသင့်နိုင်ပါသည်။"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> မှ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> သို့ ပြောင်းလိုက်ပြီ"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"မိုဘိုင်းဒေတာ"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"ဘလူးတုသ်"</item>
-    <item msgid="1160736166977503463">"အီသာနက်"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"မိုဘိုင်းဒေတာ"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"ဘလူးတုသ်"</item>
+    <item msgid="346574747471703768">"အီသာနက်"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"အမည်မသိကွန်ရက်အမျိုးအစား"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"အမည်မသိကွန်ရက်အမျိုးအစား"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-nb/strings.xml b/service/ServiceConnectivityResources/res/values-nb/strings.xml
index a561def..00a0728 100644
--- a/service/ServiceConnectivityResources/res/values-nb/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-nb/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ressurser for systemtilkobling"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Logg på Wi-Fi-nettverket"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Logg på nettverk"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Ressurser for systemtilkobling"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Logg på Wi-Fi-nettverket"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Logg på nettverk"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internettilkobling"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Trykk for å få alternativer"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilnettverket har ingen internettilgang"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Nettverket har ingen internettilgang"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Den private DNS-tjeneren kan ikke nås"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begrenset tilkobling"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Trykk for å koble til likevel"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Byttet til <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Enheten bruker <xliff:g id="NEW_NETWORK">%1$s</xliff:g> når <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ikke har Internett-tilgang. Avgifter kan påløpe."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Byttet fra <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> til <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internettilkobling"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Trykk for å få alternativer"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilnettverket har ingen internettilgang"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Nettverket har ingen internettilgang"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Den private DNS-tjeneren kan ikke nås"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begrenset tilkobling"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Trykk for å koble til likevel"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Byttet til <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Enheten bruker <xliff:g id="NEW_NETWORK">%1$s</xliff:g> når <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ikke har Internett-tilgang. Avgifter kan påløpe."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Byttet fra <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> til <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobildata"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobildata"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"en ukjent nettverkstype"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"en ukjent nettverkstype"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ne/strings.xml b/service/ServiceConnectivityResources/res/values-ne/strings.xml
index f74542d..2eaf162 100644
--- a/service/ServiceConnectivityResources/res/values-ne/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ne/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"सिस्टम कनेक्टिभिटीका स्रोतहरू"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi नेटवर्कमा साइन इन गर्नुहोस्"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"सञ्जालमा साइन इन गर्नुहोस्"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"सिस्टम कनेक्टिभिटीका स्रोतहरू"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi नेटवर्कमा साइन इन गर्नुहोस्"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"सञ्जालमा साइन इन गर्नुहोस्"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को इन्टरनेटमाथि पहुँच छैन"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"विकल्पहरूका लागि ट्याप गर्नुहोस्"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"मोबाइल नेटवर्कको इन्टरनेटमाथि पहुँच छैन"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"नेटवर्कको इन्टरनेटमाथि पहुँच छैन"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"निजी DNS सर्भरमाथि पहुँच प्राप्त गर्न सकिँदैन"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को जडान सीमित छ"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"जसरी भए पनि जडान गर्न ट्याप गर्नुहोस्"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> मा बदल्नुहोस्"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> मार्फत इन्टरनेटमाथि पहुँच राख्न नसकेको अवस्थामा यन्त्रले <xliff:g id="NEW_NETWORK">%1$s</xliff:g> प्रयोग गर्दछ। शुल्क लाग्न सक्छ।"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> बाट <xliff:g id="NEW_NETWORK">%2$s</xliff:g> मा परिवर्तन गरियो"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को इन्टरनेटमाथि पहुँच छैन"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"विकल्पहरूका लागि ट्याप गर्नुहोस्"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"मोबाइल नेटवर्कको इन्टरनेटमाथि पहुँच छैन"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"नेटवर्कको इन्टरनेटमाथि पहुँच छैन"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"निजी DNS सर्भरमाथि पहुँच प्राप्त गर्न सकिँदैन"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को जडान सीमित छ"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"जसरी भए पनि जडान गर्न ट्याप गर्नुहोस्"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> मा बदल्नुहोस्"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> मार्फत इन्टरनेटमाथि पहुँच राख्न नसकेको अवस्थामा यन्त्रले <xliff:g id="NEW_NETWORK">%1$s</xliff:g> प्रयोग गर्दछ। शुल्क लाग्न सक्छ।"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> बाट <xliff:g id="NEW_NETWORK">%2$s</xliff:g> मा परिवर्तन गरियो"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"मोबाइल डेटा"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"ब्लुटुथ"</item>
-    <item msgid="1160736166977503463">"इथरनेट"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"मोबाइल डेटा"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"ब्लुटुथ"</item>
+    <item msgid="346574747471703768">"इथरनेट"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"नेटवर्कको कुनै अज्ञात प्रकार"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"नेटवर्कको कुनै अज्ञात प्रकार"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-nl/strings.xml b/service/ServiceConnectivityResources/res/values-nl/strings.xml
index 0f3203b..394c552 100644
--- a/service/ServiceConnectivityResources/res/values-nl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-nl/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resources voor systeemconnectiviteit"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Inloggen bij wifi-netwerk"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Inloggen bij netwerk"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Resources voor systeemconnectiviteit"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Inloggen bij wifi-netwerk"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Inloggen bij netwerk"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft geen internettoegang"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tik voor opties"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiel netwerk heeft geen internettoegang"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Netwerk heeft geen internettoegang"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Geen toegang tot privé-DNS-server"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft beperkte connectiviteit"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tik om toch verbinding te maken"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Overgeschakeld naar <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Apparaat gebruikt <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wanneer <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> geen internetverbinding heeft. Er kunnen kosten in rekening worden gebracht."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Overgeschakeld van <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> naar <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft geen internettoegang"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tik voor opties"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiel netwerk heeft geen internettoegang"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Netwerk heeft geen internettoegang"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Geen toegang tot privé-DNS-server"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft beperkte connectiviteit"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tik om toch verbinding te maken"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Overgeschakeld naar <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Apparaat gebruikt <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wanneer <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> geen internetverbinding heeft. Er kunnen kosten in rekening worden gebracht."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Overgeschakeld van <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> naar <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobiele data"</item>
-    <item msgid="6341719431034774569">"Wifi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobiele data"</item>
+    <item msgid="5624324321165953608">"Wifi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"een onbekend netwerktype"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"een onbekend netwerktype"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-or/strings.xml b/service/ServiceConnectivityResources/res/values-or/strings.xml
index ecf4d69..8b85884 100644
--- a/service/ServiceConnectivityResources/res/values-or/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-or/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ସିଷ୍ଟମର ସଂଯୋଗ ସମ୍ବନ୍ଧିତ ରିସୋର୍ସଗୁଡ଼ିକ"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ୱାଇ-ଫାଇ ନେଟୱର୍କରେ ସାଇନ୍‍-ଇନ୍‍ କରନ୍ତୁ"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ନେଟ୍‌ୱର୍କରେ ସାଇନ୍‍ ଇନ୍‍ କରନ୍ତୁ"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ସିଷ୍ଟମର ସଂଯୋଗ ସମ୍ବନ୍ଧିତ ରିସୋର୍ସଗୁଡ଼ିକ"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ୱାଇ-ଫାଇ ନେଟୱର୍କରେ ସାଇନ୍‍-ଇନ୍‍ କରନ୍ତୁ"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ନେଟ୍‌ୱର୍କରେ ସାଇନ୍‍ ଇନ୍‍ କରନ୍ତୁ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ବିକଳ୍ପ ପାଇଁ ଟାପ୍‍ କରନ୍ତୁ"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"ମୋବାଇଲ୍ ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ବ୍ୟକ୍ତିଗତ DNS ସର୍ଭର୍ ଆକ୍ସେସ୍ କରିହେବ ନାହିଁ"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ସୀମିତ ସଂଯୋଗ ଅଛି"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ତଥାପି ଯୋଗାଯୋଗ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>କୁ ବଦଳାଗଲା"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ର ଇଣ୍ଟରନେଟ୍‍ ଆକ୍ସେସ୍ ନଥିବାବେଳେ ଡିଭାଇସ୍‍ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ବ୍ୟବହାର କରିଥାଏ। ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ।"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ରୁ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>କୁ ବଦଳାଗଲା"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ବିକଳ୍ପ ପାଇଁ ଟାପ୍‍ କରନ୍ତୁ"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"ମୋବାଇଲ୍ ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"ବ୍ୟକ୍ତିଗତ DNS ସର୍ଭର୍ ଆକ୍ସେସ୍ କରିହେବ ନାହିଁ"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ସୀମିତ ସଂଯୋଗ ଅଛି"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"ତଥାପି ଯୋଗାଯୋଗ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>କୁ ବଦଳାଗଲା"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ର ଇଣ୍ଟରନେଟ୍‍ ଆକ୍ସେସ୍ ନଥିବାବେଳେ ଡିଭାଇସ୍‍ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ବ୍ୟବହାର କରିଥାଏ। ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ।"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ରୁ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>କୁ ବଦଳାଗଲା"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"ମୋବାଇଲ ଡାଟା"</item>
-    <item msgid="6341719431034774569">"ୱାଇ-ଫାଇ"</item>
-    <item msgid="5081440868800877512">"ବ୍ଲୁଟୁଥ୍"</item>
-    <item msgid="1160736166977503463">"ଇଥରନେଟ୍"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"ମୋବାଇଲ ଡାଟା"</item>
+    <item msgid="5624324321165953608">"ୱାଇ-ଫାଇ"</item>
+    <item msgid="5667906231066981731">"ବ୍ଲୁଟୁଥ୍"</item>
+    <item msgid="346574747471703768">"ଇଥରନେଟ୍"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ଏକ ଅଜଣା ନେଟୱାର୍କ ପ୍ରକାର"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ଏକ ଅଜଣା ନେଟୱାର୍କ ପ୍ରକାର"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-pa/strings.xml b/service/ServiceConnectivityResources/res/values-pa/strings.xml
index 4328054..9f71cac 100644
--- a/service/ServiceConnectivityResources/res/values-pa/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pa/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ਸਿਸਟਮ ਕਨੈਕਟੀਵਿਟੀ ਸਰੋਤ"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ਵਾਈ-ਫਾਈ ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ਸਿਸਟਮ ਕਨੈਕਟੀਵਿਟੀ ਸਰੋਤ"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ਵਾਈ-ਫਾਈ ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"ਮੋਬਾਈਲ ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ਨਿੱਜੀ ਡੋਮੇਨ ਨਾਮ ਪ੍ਰਣਾਲੀ (DNS) ਸਰਵਰ \'ਤੇ ਪਹੁੰਚ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਸੀਮਤ ਕਨੈਕਟੀਵਿਟੀ ਹੈ"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ਫਿਰ ਵੀ ਕਨੈਕਟ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"ਬਦਲਕੇ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ਲਿਆਂਦਾ ਗਿਆ"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ਦੀ ਇੰਟਰਨੈੱਟ \'ਤੇ ਪਹੁੰਚ ਨਾ ਹੋਣ \'ਤੇ ਡੀਵਾਈਸ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ। ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ।"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ਤੋਂ ਬਦਲਕੇ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> \'ਤੇ ਕੀਤਾ ਗਿਆ"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"ਮੋਬਾਈਲ ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"ਨਿੱਜੀ ਡੋਮੇਨ ਨਾਮ ਪ੍ਰਣਾਲੀ (DNS) ਸਰਵਰ \'ਤੇ ਪਹੁੰਚ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਸੀਮਤ ਕਨੈਕਟੀਵਿਟੀ ਹੈ"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"ਫਿਰ ਵੀ ਕਨੈਕਟ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"ਬਦਲਕੇ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ਲਿਆਂਦਾ ਗਿਆ"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ਦੀ ਇੰਟਰਨੈੱਟ \'ਤੇ ਪਹੁੰਚ ਨਾ ਹੋਣ \'ਤੇ ਡੀਵਾਈਸ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ। ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ।"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ਤੋਂ ਬਦਲਕੇ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> \'ਤੇ ਕੀਤਾ ਗਿਆ"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"ਮੋਬਾਈਲ ਡਾਟਾ"</item>
-    <item msgid="6341719431034774569">"ਵਾਈ-ਫਾਈ"</item>
-    <item msgid="5081440868800877512">"ਬਲੂਟੁੱਥ"</item>
-    <item msgid="1160736166977503463">"ਈਥਰਨੈੱਟ"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"ਮੋਬਾਈਲ ਡਾਟਾ"</item>
+    <item msgid="5624324321165953608">"ਵਾਈ-ਫਾਈ"</item>
+    <item msgid="5667906231066981731">"ਬਲੂਟੁੱਥ"</item>
+    <item msgid="346574747471703768">"ਈਥਰਨੈੱਟ"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ਕੋਈ ਅਗਿਆਤ ਨੈੱਟਵਰਕ ਦੀ ਕਿਸਮ"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ਕੋਈ ਅਗਿਆਤ ਨੈੱਟਵਰਕ ਦੀ ਕਿਸਮ"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-pl/strings.xml b/service/ServiceConnectivityResources/res/values-pl/strings.xml
index e6b3a0c..cc84e29 100644
--- a/service/ServiceConnectivityResources/res/values-pl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pl/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Zasoby systemowe dotyczące łączności"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Zaloguj się w sieci Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Zaloguj się do sieci"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Zasoby systemowe dotyczące łączności"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Zaloguj się w sieci Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Zaloguj się do sieci"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nie ma dostępu do internetu"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Kliknij, by wyświetlić opcje"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Sieć komórkowa nie ma dostępu do internetu"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Sieć nie ma dostępu do internetu"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Brak dostępu do prywatnego serwera DNS"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ma ograniczoną łączność"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Kliknij, by mimo to nawiązać połączenie"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Zmieniono na połączenie typu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Urządzenie korzysta z połączenia typu <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, gdy <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nie dostępu do internetu. Mogą zostać naliczone opłaty."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Przełączono z połączenia typu <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na <xliff:g id="NEW_NETWORK">%2$s</xliff:g>."</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nie ma dostępu do internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Kliknij, by wyświetlić opcje"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Sieć komórkowa nie ma dostępu do internetu"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Sieć nie ma dostępu do internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Brak dostępu do prywatnego serwera DNS"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ma ograniczoną łączność"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Kliknij, by mimo to nawiązać połączenie"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Zmieniono na połączenie typu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Urządzenie korzysta z połączenia typu <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, gdy <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nie dostępu do internetu. Mogą zostać naliczone opłaty."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Przełączono z połączenia typu <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na <xliff:g id="NEW_NETWORK">%2$s</xliff:g>."</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobilna transmisja danych"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobilna transmisja danych"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nieznany typ sieci"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"nieznany typ sieci"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml
index f1d0bc0..3c15a76 100644
--- a/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividade do sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Fazer login na rede Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Fazer login na rede"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Recursos de conectividade do sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Fazer login na rede Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Fazer login na rede"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toque para ver opções"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"A rede móvel não tem acesso à Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"A rede não tem acesso à Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Não é possível acessar o servidor DNS privado"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conectividade limitada"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toque para conectar mesmo assim"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Alternado para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"O dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Esse serviço pode ser cobrado."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Alternado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toque para ver opções"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"A rede móvel não tem acesso à Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"A rede não tem acesso à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Não é possível acessar o servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conectividade limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Toque para conectar mesmo assim"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Alternado para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"O dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Esse serviço pode ser cobrado."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Alternado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"dados móveis"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"dados móveis"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"um tipo de rede desconhecido"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"um tipo de rede desconhecido"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml
index 163d70b..48dde75 100644
--- a/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conetividade do sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Iniciar sessão na rede Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Início de sessão na rede"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Recursos de conetividade do sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Iniciar sessão na rede Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Início de sessão na rede"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toque para obter mais opções"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"A rede móvel não tem acesso à Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"A rede não tem acesso à Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Não é possível aceder ao servidor DNS."</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conetividade limitada."</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toque para ligar mesmo assim."</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Mudou para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"O dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Podem aplicar-se custos."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Mudou de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toque para obter mais opções"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"A rede móvel não tem acesso à Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"A rede não tem acesso à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Não é possível aceder ao servidor DNS."</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conetividade limitada."</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Toque para ligar mesmo assim."</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Mudou para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"O dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Podem aplicar-se custos."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Mudou de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"dados móveis"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"dados móveis"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"um tipo de rede desconhecido"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"um tipo de rede desconhecido"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-pt/strings.xml b/service/ServiceConnectivityResources/res/values-pt/strings.xml
index f1d0bc0..3c15a76 100644
--- a/service/ServiceConnectivityResources/res/values-pt/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-pt/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividade do sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Fazer login na rede Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Fazer login na rede"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Recursos de conectividade do sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Fazer login na rede Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Fazer login na rede"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toque para ver opções"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"A rede móvel não tem acesso à Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"A rede não tem acesso à Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Não é possível acessar o servidor DNS privado"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conectividade limitada"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toque para conectar mesmo assim"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Alternado para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"O dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Esse serviço pode ser cobrado."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Alternado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Toque para ver opções"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"A rede móvel não tem acesso à Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"A rede não tem acesso à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Não é possível acessar o servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conectividade limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Toque para conectar mesmo assim"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Alternado para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"O dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Esse serviço pode ser cobrado."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Alternado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"dados móveis"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"dados móveis"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"um tipo de rede desconhecido"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"um tipo de rede desconhecido"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ro/strings.xml b/service/ServiceConnectivityResources/res/values-ro/strings.xml
index 221261c..fa5848f 100644
--- a/service/ServiceConnectivityResources/res/values-ro/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ro/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resurse pentru conectivitatea sistemului"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Conectați-vă la rețeaua Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Conectați-vă la rețea"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Resurse pentru conectivitatea sistemului"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Conectați-vă la rețeaua Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Conectați-vă la rețea"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nu are acces la internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Atingeți pentru opțiuni"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Rețeaua mobilă nu are acces la internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Rețeaua nu are acces la internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Serverul DNS privat nu poate fi accesat"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> are conectivitate limitată"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Atingeți pentru a vă conecta oricum"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"S-a comutat la <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Dispozitivul folosește <xliff:g id="NEW_NETWORK">%1$s</xliff:g> când <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nu are acces la internet. Se pot aplica taxe."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"S-a comutat de la <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> la <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nu are acces la internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Atingeți pentru opțiuni"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Rețeaua mobilă nu are acces la internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Rețeaua nu are acces la internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Serverul DNS privat nu poate fi accesat"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> are conectivitate limitată"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Atingeți pentru a vă conecta oricum"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"S-a comutat la <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Dispozitivul folosește <xliff:g id="NEW_NETWORK">%1$s</xliff:g> când <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nu are acces la internet. Se pot aplica taxe."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"S-a comutat de la <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> la <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"date mobile"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"date mobile"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tip de rețea necunoscut"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"un tip de rețea necunoscut"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ru/strings.xml b/service/ServiceConnectivityResources/res/values-ru/strings.xml
index ba179b7..2e074ed 100644
--- a/service/ServiceConnectivityResources/res/values-ru/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ru/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System Connectivity Resources"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Подключение к Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Регистрация в сети"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"System Connectivity Resources"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Подключение к Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Регистрация в сети"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Сеть \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" не подключена к Интернету"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Нажмите, чтобы показать варианты."</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мобильная сеть не подключена к Интернету"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Сеть не подключена к Интернету"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Доступа к частному DNS-серверу нет."</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Подключение к сети \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" ограничено"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Нажмите, чтобы подключиться"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Новое подключение: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Устройство использует <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, если подключение к сети <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> недоступно. Может взиматься плата за передачу данных."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Устройство отключено от сети <xliff:g id="NEW_NETWORK">%2$s</xliff:g> и теперь использует <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Сеть \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" не подключена к Интернету"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Нажмите, чтобы показать варианты."</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобильная сеть не подключена к Интернету"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Сеть не подключена к Интернету"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Доступа к частному DNS-серверу нет."</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Подключение к сети \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" ограничено"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Нажмите, чтобы подключиться"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Новое подключение: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Устройство использует <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, если подключение к сети <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> недоступно. Может взиматься плата за передачу данных."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Устройство отключено от сети <xliff:g id="NEW_NETWORK">%2$s</xliff:g> и теперь использует <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мобильный интернет"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мобильный интернет"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"неизвестный тип сети"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"неизвестный тип сети"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-si/strings.xml b/service/ServiceConnectivityResources/res/values-si/strings.xml
index 1c493a7..a4f720a 100644
--- a/service/ServiceConnectivityResources/res/values-si/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-si/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"පද්ධති සබැඳුම් හැකියා සම්පත්"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi ජාලයට පුරනය වන්න"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ජාලයට පුරනය වන්න"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"පද්ධති සබැඳුම් හැකියා සම්පත්"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi ජාලයට පුරනය වන්න"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ජාලයට පුරනය වන්න"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට අන්තර්ජාල ප්‍රවේශය නැත"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"විකල්ප සඳහා තට්ටු කරන්න"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"ජංගම ජාලවලට අන්තර්ජාල ප්‍රවේශය නැත"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"ජාලයට අන්තර්ජාල ප්‍රවේශය නැත"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"පුද්ගලික DNS සේවාදායකයට ප්‍රවේශ වීමට නොහැකිය"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට සීමිත සබැඳුම් හැකියාවක් ඇත"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"කෙසේ වෙතත් ඉදිරියට යාමට තට්ටු කරන්න"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> වෙත මාරු විය"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"උපාංගය <xliff:g id="NEW_NETWORK">%1$s</xliff:g> <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> සඳහා අන්තර්ජාල ප්‍රවේශය නැති විට භාවිත කරයි. ගාස්තු අදාළ විය හැකිය."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> සිට <xliff:g id="NEW_NETWORK">%2$s</xliff:g> වෙත මාරු විය"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට අන්තර්ජාල ප්‍රවේශය නැත"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"විකල්ප සඳහා තට්ටු කරන්න"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"ජංගම ජාලවලට අන්තර්ජාල ප්‍රවේශය නැත"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"ජාලයට අන්තර්ජාල ප්‍රවේශය නැත"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"පුද්ගලික DNS සේවාදායකයට ප්‍රවේශ වීමට නොහැකිය"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට සීමිත සබැඳුම් හැකියාවක් ඇත"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"කෙසේ වෙතත් ඉදිරියට යාමට තට්ටු කරන්න"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> වෙත මාරු විය"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"උපාංගය <xliff:g id="NEW_NETWORK">%1$s</xliff:g> <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> සඳහා අන්තර්ජාල ප්‍රවේශය නැති විට භාවිත කරයි. ගාස්තු අදාළ විය හැකිය."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> සිට <xliff:g id="NEW_NETWORK">%2$s</xliff:g> වෙත මාරු විය"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"ජංගම දත්ත"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"බ්ලූටූත්"</item>
-    <item msgid="1160736166977503463">"ඊතර්නෙට්"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"ජංගම දත්ත"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"බ්ලූටූත්"</item>
+    <item msgid="346574747471703768">"ඊතර්නෙට්"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"නොදන්නා ජාල වර්ගයකි"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"නොදන්නා ජාල වර්ගයකි"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-sk/strings.xml b/service/ServiceConnectivityResources/res/values-sk/strings.xml
index 1b9313a..432b670 100644
--- a/service/ServiceConnectivityResources/res/values-sk/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sk/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Zdroje možností pripojenia systému"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prihlásiť sa do siete Wi‑Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Prihlásenie do siete"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Zdroje možností pripojenia systému"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Prihlásiť sa do siete Wi‑Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Prihlásenie do siete"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá prístup k internetu"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Klepnutím získate možnosti"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilná sieť nemá prístup k internetu"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Sieť nemá prístup k internetu"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"K súkromnému serveru DNS sa nepodarilo získať prístup"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> má obmedzené pripojenie"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ak sa chcete aj napriek tomu pripojiť, klepnite"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Prepnuté na sieť: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Keď <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nemá prístup k internetu, zariadenie používa <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Môžu sa účtovať poplatky."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Prepnuté zo siete <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na sieť <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá prístup k internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Klepnutím získate možnosti"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilná sieť nemá prístup k internetu"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Sieť nemá prístup k internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"K súkromnému serveru DNS sa nepodarilo získať prístup"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> má obmedzené pripojenie"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Ak sa chcete aj napriek tomu pripojiť, klepnite"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Prepnuté na sieť: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Keď <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nemá prístup k internetu, zariadenie používa <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Môžu sa účtovať poplatky."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Prepnuté zo siete <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na sieť <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobilné dáta"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobilné dáta"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"neznámy typ siete"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"neznámy typ siete"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-sl/strings.xml b/service/ServiceConnectivityResources/res/values-sl/strings.xml
index 739fb8e..b727614 100644
--- a/service/ServiceConnectivityResources/res/values-sl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sl/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Viri povezljivosti sistema"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prijavite se v omrežje Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Prijava v omrežje"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Viri povezljivosti sistema"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Prijavite se v omrežje Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Prijava v omrežje"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Omrežje <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nima dostopa do interneta"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Dotaknite se za možnosti"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilno omrežje nima dostopa do interneta"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Omrežje nima dostopa do interneta"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Do zasebnega strežnika DNS ni mogoče dostopati"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Povezljivost omrežja <xliff:g id="NETWORK_SSID">%1$s</xliff:g> je omejena"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Dotaknite se, da kljub temu vzpostavite povezavo"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Preklopljeno na omrežje vrste <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Naprava uporabi omrežje vrste <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, ko omrežje vrste <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nima dostopa do interneta. Prenos podatkov se lahko zaračuna."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Preklopljeno z omrežja vrste <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na omrežje vrste <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Omrežje <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nima dostopa do interneta"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Dotaknite se za možnosti"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilno omrežje nima dostopa do interneta"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Omrežje nima dostopa do interneta"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Do zasebnega strežnika DNS ni mogoče dostopati"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Povezljivost omrežja <xliff:g id="NETWORK_SSID">%1$s</xliff:g> je omejena"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Dotaknite se, da kljub temu vzpostavite povezavo"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Preklopljeno na omrežje vrste <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Naprava uporabi omrežje vrste <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, ko omrežje vrste <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nima dostopa do interneta. Prenos podatkov se lahko zaračuna."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Preklopljeno z omrežja vrste <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na omrežje vrste <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"prenos podatkov v mobilnem omrežju"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"prenos podatkov v mobilnem omrežju"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"neznana vrsta omrežja"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"neznana vrsta omrežja"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-sq/strings.xml b/service/ServiceConnectivityResources/res/values-sq/strings.xml
index cf8cf3b..385c75c 100644
--- a/service/ServiceConnectivityResources/res/values-sq/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sq/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Burimet e lidhshmërisë së sistemit"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Identifikohu në rrjetin Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Identifikohu në rrjet"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Burimet e lidhshmërisë së sistemit"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Identifikohu në rrjetin Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Identifikohu në rrjet"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nuk ka qasje në internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Trokit për opsionet"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Rrjeti celular nuk ka qasje në internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Rrjeti nuk ka qasje në internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Serveri privat DNS nuk mund të qaset"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ka lidhshmëri të kufizuar"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Trokit për t\'u lidhur gjithsesi"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Kaloi te <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Pajisja përdor <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kur <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nuk ka qasje në internet. Mund të zbatohen tarifa."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Kaloi nga <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> te <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nuk ka qasje në internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Trokit për opsionet"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Rrjeti celular nuk ka qasje në internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Rrjeti nuk ka qasje në internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Serveri privat DNS nuk mund të qaset"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ka lidhshmëri të kufizuar"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Trokit për t\'u lidhur gjithsesi"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Kaloi te <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Pajisja përdor <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kur <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nuk ka qasje në internet. Mund të zbatohen tarifa."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Kaloi nga <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> te <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"të dhënat celulare"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Eternet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"të dhënat celulare"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Eternet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"një lloj rrjeti i panjohur"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"një lloj rrjeti i panjohur"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-sr/strings.xml b/service/ServiceConnectivityResources/res/values-sr/strings.xml
index 1f7c95c..928dc79 100644
--- a/service/ServiceConnectivityResources/res/values-sr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sr/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ресурси за повезивање са системом"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Пријављивање на WiFi мрежу"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Пријавите се на мрежу"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Ресурси за повезивање са системом"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Пријављивање на WiFi мрежу"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Пријавите се на мрежу"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема приступ интернету"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Додирните за опције"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мобилна мрежа нема приступ интернету"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Мрежа нема приступ интернету"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Приступ приватном DNS серверу није успео"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничену везу"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Додирните да бисте се ипак повезали"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Прешли сте на тип мреже <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Уређај користи тип мреже <xliff:g id="NEW_NETWORK">%1$s</xliff:g> када тип мреже <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> нема приступ интернету. Можда ће се наплаћивати трошкови."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Прешли сте са типа мреже <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на тип мреже <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема приступ интернету"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Додирните за опције"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобилна мрежа нема приступ интернету"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Мрежа нема приступ интернету"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Приступ приватном DNS серверу није успео"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничену везу"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Додирните да бисте се ипак повезали"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Прешли сте на тип мреже <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Уређај користи тип мреже <xliff:g id="NEW_NETWORK">%1$s</xliff:g> када тип мреже <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> нема приступ интернету. Можда ће се наплаћивати трошкови."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Прешли сте са типа мреже <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на тип мреже <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мобилни подаци"</item>
-    <item msgid="6341719431034774569">"WiFi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Етернет"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мобилни подаци"</item>
+    <item msgid="5624324321165953608">"WiFi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Етернет"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"непознат тип мреже"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"непознат тип мреже"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-sv/strings.xml b/service/ServiceConnectivityResources/res/values-sv/strings.xml
index 57e74e9..d714124 100644
--- a/service/ServiceConnectivityResources/res/values-sv/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sv/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resurser för systemanslutning"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Logga in på ett wifi-nätverk"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Logga in på nätverket"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Resurser för systemanslutning"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Logga in på ett wifi-nätverk"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Logga in på nätverket"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetanslutning"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tryck för alternativ"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilnätverket har ingen internetanslutning"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Nätverket har ingen internetanslutning"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Det går inte att komma åt den privata DNS-servern."</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begränsad anslutning"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tryck för att ansluta ändå"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Byte av nätverk till <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> används på enheten när det inte finns internetåtkomst via <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Avgifter kan tillkomma."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Byte av nätverk från <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> till <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetanslutning"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Tryck för alternativ"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilnätverket har ingen internetanslutning"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Nätverket har ingen internetanslutning"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Det går inte att komma åt den privata DNS-servern."</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begränsad anslutning"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tryck för att ansluta ändå"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Byte av nätverk till <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> används på enheten när det inte finns internetåtkomst via <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Avgifter kan tillkomma."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Byte av nätverk från <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> till <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobildata"</item>
-    <item msgid="6341719431034774569">"Wifi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobildata"</item>
+    <item msgid="5624324321165953608">"Wifi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"en okänd nätverkstyp"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"en okänd nätverkstyp"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-sw/strings.xml b/service/ServiceConnectivityResources/res/values-sw/strings.xml
index 5c4d594..15d6cab 100644
--- a/service/ServiceConnectivityResources/res/values-sw/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sw/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Nyenzo za Muunganisho wa Mfumo"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Ingia kwa mtandao wa Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Ingia katika mtandao"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Nyenzo za Muunganisho wa Mfumo"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Ingia kwa mtandao wa Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Ingia katika mtandao"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina uwezo wa kufikia intaneti"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Gusa ili upate chaguo"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mtandao wa simu hauna uwezo wa kufikia intaneti"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Mtandao hauna uwezo wa kufikia intaneti"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Seva ya faragha ya DNS haiwezi kufikiwa"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ina muunganisho unaofikia huduma chache."</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Gusa ili uunganishe tu"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Sasa inatumia <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Kifaa hutumia <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wakati <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> haina intaneti. Huenda ukalipishwa."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Imebadilisha mtandao kutoka <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na sasa inatumia <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina uwezo wa kufikia intaneti"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Gusa ili upate chaguo"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mtandao wa simu hauna uwezo wa kufikia intaneti"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Mtandao hauna uwezo wa kufikia intaneti"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Seva ya faragha ya DNS haiwezi kufikiwa"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ina muunganisho unaofikia huduma chache."</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Gusa ili uunganishe tu"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Sasa inatumia <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Kifaa hutumia <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wakati <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> haina intaneti. Huenda ukalipishwa."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Imebadilisha mtandao kutoka <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na sasa inatumia <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"data ya mtandao wa simu"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethaneti"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"data ya mtandao wa simu"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethaneti"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"aina ya mtandao isiyojulikana"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"aina ya mtandao isiyojulikana"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ta/strings.xml b/service/ServiceConnectivityResources/res/values-ta/strings.xml
index 90f89c9..9850a35 100644
--- a/service/ServiceConnectivityResources/res/values-ta/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ta/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"சிஸ்டம் இணைப்பு மூலங்கள்"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"வைஃபை நெட்வொர்க்கில் உள்நுழையவும்"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"நெட்வொர்க்கில் உள்நுழையவும்"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"சிஸ்டம் இணைப்பு தகவல்கள்"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"வைஃபை நெட்வொர்க்கில் உள்நுழையவும்"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"நெட்வொர்க்கில் உள்நுழையவும்"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"விருப்பங்களுக்கு, தட்டவும்"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"மொபைல் நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"தனிப்பட்ட DNS சேவையகத்தை அணுக இயலாது"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> வரம்பிற்கு உட்பட்ட இணைப்புநிலையைக் கொண்டுள்ளது"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"எப்படியேனும் இணைப்பதற்குத் தட்டவும்"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>க்கு மாற்றப்பட்டது"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> நெட்வொர்க்கில் இண்டர்நெட் அணுகல் இல்லாததால், சாதனமானது <xliff:g id="NEW_NETWORK">%1$s</xliff:g> நெட்வொர்க்கைப் பயன்படுத்துகிறது. கட்டணங்கள் விதிக்கப்படலாம்."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> இலிருந்து <xliff:g id="NEW_NETWORK">%2$s</xliff:g>க்கு மாற்றப்பட்டது"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"விருப்பங்களுக்கு, தட்டவும்"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"மொபைல் நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"தனிப்பட்ட DNS சேவையகத்தை அணுக இயலாது"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> வரம்பிற்கு உட்பட்ட இணைப்புநிலையைக் கொண்டுள்ளது"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"எப்படியேனும் இணைப்பதற்குத் தட்டவும்"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>க்கு மாற்றப்பட்டது"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> நெட்வொர்க்கில் இண்டர்நெட் அணுகல் இல்லாததால், சாதனமானது <xliff:g id="NEW_NETWORK">%1$s</xliff:g> நெட்வொர்க்கைப் பயன்படுத்துகிறது. கட்டணங்கள் விதிக்கப்படலாம்."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> இலிருந்து <xliff:g id="NEW_NETWORK">%2$s</xliff:g>க்கு மாற்றப்பட்டது"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"மொபைல் டேட்டா"</item>
-    <item msgid="6341719431034774569">"வைஃபை"</item>
-    <item msgid="5081440868800877512">"புளூடூத்"</item>
-    <item msgid="1160736166977503463">"ஈதர்நெட்"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"மொபைல் டேட்டா"</item>
+    <item msgid="5624324321165953608">"வைஃபை"</item>
+    <item msgid="5667906231066981731">"புளூடூத்"</item>
+    <item msgid="346574747471703768">"ஈதர்நெட்"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"தெரியாத நெட்வொர்க் வகை"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"தெரியாத நெட்வொர்க் வகை"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-te/strings.xml b/service/ServiceConnectivityResources/res/values-te/strings.xml
index c69b599..f7182a8 100644
--- a/service/ServiceConnectivityResources/res/values-te/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-te/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"సిస్టమ్ కనెక్టివిటీ రిసోర్స్‌లు"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"సిస్టమ్ కనెక్టివిటీ రిసోర్స్‌లు"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>కి ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ఎంపికల కోసం నొక్కండి"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"మొబైల్ నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ప్రైవేట్ DNS సర్వర్‌ను యాక్సెస్ చేయడం సాధ్యపడదు"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> పరిమిత కనెక్టివిటీని కలిగి ఉంది"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ఏదేమైనా కనెక్ట్ చేయడానికి నొక్కండి"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>కి మార్చబడింది"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"పరికరం <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>కి ఇంటర్నెట్ యాక్సెస్ లేనప్పుడు <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ని ఉపయోగిస్తుంది. ఛార్జీలు వర్తించవచ్చు."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> నుండి <xliff:g id="NEW_NETWORK">%2$s</xliff:g>కి మార్చబడింది"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>కి ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"ఎంపికల కోసం నొక్కండి"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"మొబైల్ నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"ప్రైవేట్ DNS సర్వర్‌ను యాక్సెస్ చేయడం సాధ్యపడదు"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> పరిమిత కనెక్టివిటీని కలిగి ఉంది"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"ఏదేమైనా కనెక్ట్ చేయడానికి నొక్కండి"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>కి మార్చబడింది"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"పరికరం <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>కి ఇంటర్నెట్ యాక్సెస్ లేనప్పుడు <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ని ఉపయోగిస్తుంది. ఛార్జీలు వర్తించవచ్చు."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> నుండి <xliff:g id="NEW_NETWORK">%2$s</xliff:g>కి మార్చబడింది"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"మొబైల్ డేటా"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"బ్లూటూత్"</item>
-    <item msgid="1160736166977503463">"ఈథర్‌నెట్"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"మొబైల్ డేటా"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"బ్లూటూత్"</item>
+    <item msgid="346574747471703768">"ఈథర్‌నెట్"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"తెలియని నెట్‌వర్క్ రకం"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"తెలియని నెట్‌వర్క్ రకం"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-th/strings.xml b/service/ServiceConnectivityResources/res/values-th/strings.xml
index eee5a35..7049309 100644
--- a/service/ServiceConnectivityResources/res/values-th/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-th/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ทรัพยากรการเชื่อมต่อของระบบ"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"ลงชื่อเข้าใช้เครือข่าย WiFi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"ลงชื่อเข้าใช้เครือข่าย"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ทรัพยากรการเชื่อมต่อของระบบ"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ลงชื่อเข้าใช้เครือข่าย WiFi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"ลงชื่อเข้าใช้เครือข่าย"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> เข้าถึงอินเทอร์เน็ตไม่ได้"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"แตะเพื่อดูตัวเลือก"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"เครือข่ายมือถือไม่มีการเข้าถึงอินเทอร์เน็ต"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"เครือข่ายไม่มีการเข้าถึงอินเทอร์เน็ต"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"เข้าถึงเซิร์ฟเวอร์ DNS ไม่ได้"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> มีการเชื่อมต่อจำกัด"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"แตะเพื่อเชื่อมต่อ"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"เปลี่ยนเป็น <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"อุปกรณ์จะใช้ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> เมื่อ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> เข้าถึงอินเทอร์เน็ตไม่ได้ โดยอาจมีค่าบริการ"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"เปลี่ยนจาก <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> เป็น <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> เข้าถึงอินเทอร์เน็ตไม่ได้"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"แตะเพื่อดูตัวเลือก"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"เครือข่ายมือถือไม่มีการเข้าถึงอินเทอร์เน็ต"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"เครือข่ายไม่มีการเข้าถึงอินเทอร์เน็ต"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"เข้าถึงเซิร์ฟเวอร์ DNS ไม่ได้"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> มีการเชื่อมต่อจำกัด"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"แตะเพื่อเชื่อมต่อ"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"เปลี่ยนเป็น <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"อุปกรณ์จะใช้ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> เมื่อ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> เข้าถึงอินเทอร์เน็ตไม่ได้ โดยอาจมีค่าบริการ"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"เปลี่ยนจาก <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> เป็น <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"อินเทอร์เน็ตมือถือ"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"บลูทูธ"</item>
-    <item msgid="1160736166977503463">"อีเทอร์เน็ต"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"อินเทอร์เน็ตมือถือ"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"บลูทูธ"</item>
+    <item msgid="346574747471703768">"อีเทอร์เน็ต"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ประเภทเครือข่ายที่ไม่รู้จัก"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"ประเภทเครือข่ายที่ไม่รู้จัก"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-tl/strings.xml b/service/ServiceConnectivityResources/res/values-tl/strings.xml
index 8d665fe..c866fd4 100644
--- a/service/ServiceConnectivityResources/res/values-tl/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-tl/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Mga Resource ng Pagkakonekta ng System"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Mag-sign in sa Wi-Fi network"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Mag-sign in sa network"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Mga Resource ng Pagkakonekta ng System"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Mag-sign in sa Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Mag-sign in sa network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Walang access sa internet ang <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"I-tap para sa mga opsyon"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Walang access sa internet ang mobile network"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Walang access sa internet ang network"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Hindi ma-access ang pribadong DNS server"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Limitado ang koneksyon ng <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"I-tap para kumonekta pa rin"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Lumipat sa <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Ginagamit ng device ang <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kapag walang access sa internet ang <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Maaaring may mga malapat na singilin."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Lumipat sa <xliff:g id="NEW_NETWORK">%2$s</xliff:g> mula sa <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Walang access sa internet ang <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"I-tap para sa mga opsyon"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Walang access sa internet ang mobile network"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Walang access sa internet ang network"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Hindi ma-access ang pribadong DNS server"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Limitado ang koneksyon ng <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"I-tap para kumonekta pa rin"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Lumipat sa <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Ginagamit ng device ang <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kapag walang access sa internet ang <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Maaaring may mga malapat na singilin."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Lumipat sa <xliff:g id="NEW_NETWORK">%2$s</xliff:g> mula sa <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobile data"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobile data"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"isang hindi kilalang uri ng network"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"isang hindi kilalang uri ng network"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-tr/strings.xml b/service/ServiceConnectivityResources/res/values-tr/strings.xml
index cfb7632..c4930a8 100644
--- a/service/ServiceConnectivityResources/res/values-tr/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-tr/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sistem Bağlantı Kaynakları"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Kablosuz ağda oturum açın"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Ağda oturum açın"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Sistem Bağlantı Kaynakları"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Kablosuz ağda oturum açın"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Ağda oturum açın"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ağının internet bağlantısı yok"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Seçenekler için dokunun"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobil ağın internet bağlantısı yok"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Ağın internet bağlantısı yok"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Gizli DNS sunucusuna erişilemiyor"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> sınırlı bağlantıya sahip"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Yine de bağlanmak için dokunun"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ağına geçildi"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ağının internet erişimi olmadığında cihaz <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ağını kullanır. Bunun için ödeme alınabilir."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ağından <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ağına geçildi"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ağının internet bağlantısı yok"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Seçenekler için dokunun"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobil ağın internet bağlantısı yok"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Ağın internet bağlantısı yok"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Gizli DNS sunucusuna erişilemiyor"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> sınırlı bağlantıya sahip"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Yine de bağlanmak için dokunun"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ağına geçildi"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ağının internet erişimi olmadığında cihaz <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ağını kullanır. Bunun için ödeme alınabilir."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ağından <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ağına geçildi"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobil veri"</item>
-    <item msgid="6341719431034774569">"Kablosuz"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobil veri"</item>
+    <item msgid="5624324321165953608">"Kablosuz"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"bilinmeyen ağ türü"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"bilinmeyen ağ türü"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-uk/strings.xml b/service/ServiceConnectivityResources/res/values-uk/strings.xml
index c5da746..8811263 100644
--- a/service/ServiceConnectivityResources/res/values-uk/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-uk/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ресурси для підключення системи"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Вхід у мережу Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Вхід у мережу"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Ресурси для підключення системи"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Вхід у мережу Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Вхід у мережу"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"Мережа <xliff:g id="NETWORK_SSID">%1$s</xliff:g> не має доступу до Інтернету"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Торкніться, щоб відкрити опції"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Мобільна мережа не має доступу до Інтернету"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Мережа не має доступу до Інтернету"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Немає доступу до приватного DNS-сервера"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"Підключення до мережі <xliff:g id="NETWORK_SSID">%1$s</xliff:g> обмежено"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Натисніть, щоб усе одно підключитися"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Пристрій перейшов на мережу <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Коли мережа <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> не має доступу до Інтернету, використовується <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Може стягуватися плата."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Пристрій перейшов з мережі <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на мережу <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"Мережа <xliff:g id="NETWORK_SSID">%1$s</xliff:g> не має доступу до Інтернету"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Торкніться, щоб відкрити опції"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Мобільна мережа не має доступу до Інтернету"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Мережа не має доступу до Інтернету"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Немає доступу до приватного DNS-сервера"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"Підключення до мережі <xliff:g id="NETWORK_SSID">%1$s</xliff:g> обмежено"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Натисніть, щоб усе одно підключитися"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Пристрій перейшов на мережу <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Коли мережа <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> не має доступу до Інтернету, використовується <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Може стягуватися плата."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Пристрій перейшов з мережі <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на мережу <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"мобільний Інтернет"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"мобільний Інтернет"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"невідомий тип мережі"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"невідомий тип мережі"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-ur/strings.xml b/service/ServiceConnectivityResources/res/values-ur/strings.xml
index bd2a228..8f9656c 100644
--- a/service/ServiceConnectivityResources/res/values-ur/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ur/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"سسٹم کنیکٹوٹی کے وسائل"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"‏Wi-Fi نیٹ ورک میں سائن ان کریں"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"نیٹ ورک میں سائن ان کریں"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"سسٹم کنیکٹوٹی کے وسائل"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"‏Wi-Fi نیٹ ورک میں سائن ان کریں"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"نیٹ ورک میں سائن ان کریں"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"اختیارات کیلئے تھپتھپائیں"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"موبائل نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"‏نجی DNS سرور تک رسائی حاصل نہیں کی جا سکی"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> کی کنیکٹوٹی محدود ہے"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"بہر حال منسلک کرنے کے لیے تھپتھپائیں"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> پر سوئچ ہو گیا"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"جب <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> کو انٹرنیٹ تک رسائی نہیں ہوتی ہے تو آلہ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> کا استعمال کرتا ہے۔ چارجز لاگو ہو سکتے ہیں۔"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> سے <xliff:g id="NEW_NETWORK">%2$s</xliff:g> پر سوئچ ہو گیا"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"اختیارات کیلئے تھپتھپائیں"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"موبائل نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"‏نجی DNS سرور تک رسائی حاصل نہیں کی جا سکی"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> کی کنیکٹوٹی محدود ہے"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"بہر حال منسلک کرنے کے لیے تھپتھپائیں"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> پر سوئچ ہو گیا"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"جب <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> کو انٹرنیٹ تک رسائی نہیں ہوتی ہے تو آلہ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> کا استعمال کرتا ہے۔ چارجز لاگو ہو سکتے ہیں۔"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> سے <xliff:g id="NEW_NETWORK">%2$s</xliff:g> پر سوئچ ہو گیا"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"موبائل ڈیٹا"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"بلوٹوتھ"</item>
-    <item msgid="1160736166977503463">"ایتھرنیٹ"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"موبائل ڈیٹا"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"بلوٹوتھ"</item>
+    <item msgid="346574747471703768">"ایتھرنیٹ"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"نامعلوم نیٹ ورک کی قسم"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"نامعلوم نیٹ ورک کی قسم"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-uz/strings.xml b/service/ServiceConnectivityResources/res/values-uz/strings.xml
index 567aa88..d7285ad 100644
--- a/service/ServiceConnectivityResources/res/values-uz/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-uz/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Tizim aloqa resurslari"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi tarmoqqa kirish"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Tarmoqqa kirish"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Tizim aloqa resurslari"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi tarmoqqa kirish"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Tarmoqqa kirish"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda internetga ruxsati yoʻq"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Variantlarni ko‘rsatish uchun bosing"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mobil tarmoq internetga ulanmagan"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Tarmoq internetga ulanmagan"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Xususiy DNS server ishlamayapti"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda aloqa cheklangan"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Baribir ulash uchun bosing"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Yangi ulanish: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Agar <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tarmoqda internet uzilsa, qurilma <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ga ulanadi. Sarflangan trafik uchun haq olinishi mumkin."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> tarmog‘idan <xliff:g id="NEW_NETWORK">%2$s</xliff:g> tarmog‘iga o‘tildi"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda internetga ruxsati yoʻq"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Variantlarni ko‘rsatish uchun bosing"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mobil tarmoq internetga ulanmagan"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Tarmoq internetga ulanmagan"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Xususiy DNS server ishlamayapti"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda aloqa cheklangan"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Baribir ulash uchun bosing"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Yangi ulanish: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Agar <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tarmoqda internet uzilsa, qurilma <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ga ulanadi. Sarflangan trafik uchun haq olinishi mumkin."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> tarmog‘idan <xliff:g id="NEW_NETWORK">%2$s</xliff:g> tarmog‘iga o‘tildi"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"mobil internet"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"mobil internet"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nomaʼlum tarmoq turi"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"nomaʼlum tarmoq turi"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-vi/strings.xml b/service/ServiceConnectivityResources/res/values-vi/strings.xml
index 590b388..239fb81 100644
--- a/service/ServiceConnectivityResources/res/values-vi/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-vi/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Tài nguyên kết nối hệ thống"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Đăng nhập vào mạng Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Đăng nhập vào mạng"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Tài nguyên kết nối hệ thống"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Đăng nhập vào mạng Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Đăng nhập vào mạng"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> không có quyền truy cập Internet"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Nhấn để biết tùy chọn"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Mạng di động không có quyền truy cập Internet"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Mạng không có quyền truy cập Internet"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Không thể truy cập máy chủ DNS riêng tư"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> có khả năng kết nối giới hạn"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Nhấn để tiếp tục kết nối"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Đã chuyển sang <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Thiết bị sử dụng <xliff:g id="NEW_NETWORK">%1$s</xliff:g> khi <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> không có quyền truy cập Internet. Bạn có thể phải trả phí."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Đã chuyển từ <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> sang <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> không có quyền truy cập Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Nhấn để biết tùy chọn"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Mạng di động không có quyền truy cập Internet"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Mạng không có quyền truy cập Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Không thể truy cập máy chủ DNS riêng tư"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> có khả năng kết nối giới hạn"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Nhấn để tiếp tục kết nối"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Đã chuyển sang <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Thiết bị sử dụng <xliff:g id="NEW_NETWORK">%1$s</xliff:g> khi <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> không có quyền truy cập Internet. Bạn có thể phải trả phí."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Đã chuyển từ <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> sang <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"dữ liệu di động"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"Bluetooth"</item>
-    <item msgid="1160736166977503463">"Ethernet"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"dữ liệu di động"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="346574747471703768">"Ethernet"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"loại mạng không xác định"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"loại mạng không xác định"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml
index 9d6cff9..e318c0b 100644
--- a/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"系统网络连接资源"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"登录到WLAN网络"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"登录到网络"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"系统网络连接资源"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"登录到WLAN网络"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"登录到网络"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 无法访问互联网"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"点按即可查看相关选项"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"此移动网络无法访问互联网"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"此网络无法访问互联网"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"无法访问私人 DNS 服务器"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的连接受限"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"点按即可继续连接"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"已切换至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"设备会在<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>无法访问互联网时使用<xliff:g id="NEW_NETWORK">%1$s</xliff:g>(可能需要支付相应的费用)。"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"已从<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>切换至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 无法访问互联网"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"点按即可查看相关选项"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"此移动网络无法访问互联网"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"此网络无法访问互联网"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"无法访问私人 DNS 服务器"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的连接受限"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"点按即可继续连接"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"已切换至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"设备会在<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>无法访问互联网时使用<xliff:g id="NEW_NETWORK">%1$s</xliff:g>(可能需要支付相应的费用)。"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"已从<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>切换至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"移动数据"</item>
-    <item msgid="6341719431034774569">"WLAN"</item>
-    <item msgid="5081440868800877512">"蓝牙"</item>
-    <item msgid="1160736166977503463">"以太网"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"移动数据"</item>
+    <item msgid="5624324321165953608">"WLAN"</item>
+    <item msgid="5667906231066981731">"蓝牙"</item>
+    <item msgid="346574747471703768">"以太网"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"未知网络类型"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"未知网络类型"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml
index c84241c..af3dccd 100644
--- a/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"系統連線資源"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"登入 Wi-Fi 網絡"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"登入網絡"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"系統連線資源"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"登入 Wi-Fi 網絡"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"登入網絡"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>未有連接至互聯網"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"輕按即可查看選項"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"流動網絡並未連接互聯網"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"網絡並未連接互聯網"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"無法存取私人 DNS 伺服器"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>連線受限"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"仍要輕按以連結至此網絡"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"已切換至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"裝置會在 <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> 無法連線至互聯網時使用<xliff:g id="NEW_NETWORK">%1$s</xliff:g> (可能需要支付相關費用)。"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"已從<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>切換至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>未有連接至互聯網"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"輕按即可查看選項"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"流動網絡並未連接互聯網"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"網絡並未連接互聯網"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"無法存取私人 DNS 伺服器"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>連線受限"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"仍要輕按以連結至此網絡"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"已切換至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"裝置會在 <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> 無法連線至互聯網時使用<xliff:g id="NEW_NETWORK">%1$s</xliff:g> (可能需要支付相關費用)。"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"已從<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>切換至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"流動數據"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"藍牙"</item>
-    <item msgid="1160736166977503463">"以太網絡"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"流動數據"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"藍牙"</item>
+    <item msgid="346574747471703768">"以太網絡"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"不明網絡類型"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"不明網絡類型"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml
index 07540d1..6441707 100644
--- a/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"系統連線資源"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"登入 Wi-Fi 網路"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"登入網路"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"系統連線資源"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"登入 Wi-Fi 網路"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"登入網路"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 沒有網際網路連線"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"輕觸即可查看選項"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"這個行動網路沒有網際網路連線"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"這個網路沒有網際網路連線"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"無法存取私人 DNS 伺服器"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的連線能力受限"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"輕觸即可繼續連線"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"已切換至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"裝置會在無法連上「<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>」時切換至「<xliff:g id="NEW_NETWORK">%1$s</xliff:g>」(可能需要支付相關費用)。"</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"已從 <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> 切換至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 沒有網際網路連線"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"輕觸即可查看選項"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"這個行動網路沒有網際網路連線"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"這個網路沒有網際網路連線"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"無法存取私人 DNS 伺服器"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的連線能力受限"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"輕觸即可繼續連線"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"已切換至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"裝置會在無法連上「<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>」時切換至「<xliff:g id="NEW_NETWORK">%1$s</xliff:g>」(可能需要支付相關費用)。"</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"已從 <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> 切換至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"行動數據"</item>
-    <item msgid="6341719431034774569">"Wi-Fi"</item>
-    <item msgid="5081440868800877512">"藍牙"</item>
-    <item msgid="1160736166977503463">"乙太網路"</item>
-    <item msgid="7347618872551558605">"VPN"</item>
+    <item msgid="3004933964374161223">"行動數據"</item>
+    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5667906231066981731">"藍牙"</item>
+    <item msgid="346574747471703768">"乙太網路"</item>
+    <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"不明的網路類型"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"不明的網路類型"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values-zu/strings.xml b/service/ServiceConnectivityResources/res/values-zu/strings.xml
index 19f390b..b59f0d1 100644
--- a/service/ServiceConnectivityResources/res/values-zu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-zu/strings.xml
@@ -17,27 +17,27 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Izinsiza Zokuxhumeka Zesistimu"</string>
-    <string name="wifi_available_sign_in" msgid="5254156478006453593">"Ngena ngemvume kunethiwekhi ye-Wi-Fi"</string>
-    <string name="network_available_sign_in" msgid="7794369329839408792">"Ngena ngemvume kunethiwekhi"</string>
-    <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) -->
+    <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Izinsiza Zokuxhumeka Zesistimu"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Ngena ngemvume kunethiwekhi ye-Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="2622520134876355561">"Ngena ngemvume kunethiwekhi"</string>
+    <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="3961697321010262514">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ayinakho ukufinyelela kwe-inthanethi"</string>
-    <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Thepha ukuze uthole izinketho"</string>
-    <string name="mobile_no_internet" msgid="2262524005014119639">"Inethiwekhi yeselula ayinakho ukufinyelela kwe-inthanethi"</string>
-    <string name="other_networks_no_internet" msgid="8226004998719563755">"Inethiwekhi ayinakho ukufinyelela kwenethiwekhi"</string>
-    <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Iseva eyimfihlo ye-DNS ayikwazi ukufinyelelwa"</string>
-    <string name="network_partial_connectivity" msgid="5957065286265771273">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> inokuxhumeka okukhawulelwe"</string>
-    <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Thepha ukuze uxhume noma kunjalo"</string>
-    <string name="network_switch_metered" msgid="2814798852883117872">"Kushintshelwe ku-<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
-    <string name="network_switch_metered_detail" msgid="605546931076348229">"Idivayisi isebenzisa i-<xliff:g id="NEW_NETWORK">%1$s</xliff:g> uma i-<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> inganakho ukufinyelela kwe-inthanethi. Kungasebenza izindleko."</string>
-    <string name="network_switch_metered_toast" msgid="8831325515040986641">"Kushintshelewe kusuka ku-<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> kuya ku-<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ayinakho ukufinyelela kwe-inthanethi"</string>
+    <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Thepha ukuze uthole izinketho"</string>
+    <string name="mobile_no_internet" msgid="4087718456753201450">"Inethiwekhi yeselula ayinakho ukufinyelela kwe-inthanethi"</string>
+    <string name="other_networks_no_internet" msgid="5693932964749676542">"Inethiwekhi ayinakho ukufinyelela kwenethiwekhi"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Iseva eyimfihlo ye-DNS ayikwazi ukufinyelelwa"</string>
+    <string name="network_partial_connectivity" msgid="5549503845834993258">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> inokuxhumeka okukhawulelwe"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Thepha ukuze uxhume noma kunjalo"</string>
+    <string name="network_switch_metered" msgid="5016937523571166319">"Kushintshelwe ku-<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1257300152739542096">"Idivayisi isebenzisa i-<xliff:g id="NEW_NETWORK">%1$s</xliff:g> uma i-<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> inganakho ukufinyelela kwe-inthanethi. Kungasebenza izindleko."</string>
+    <string name="network_switch_metered_toast" msgid="70691146054130335">"Kushintshelewe kusuka ku-<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> kuya ku-<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
-    <item msgid="5454013645032700715">"idatha yeselula"</item>
-    <item msgid="6341719431034774569">"I-Wi-Fi"</item>
-    <item msgid="5081440868800877512">"I-Bluetooth"</item>
-    <item msgid="1160736166977503463">"I-Ethernet"</item>
-    <item msgid="7347618872551558605">"I-VPN"</item>
+    <item msgid="3004933964374161223">"idatha yeselula"</item>
+    <item msgid="5624324321165953608">"I-Wi-Fi"</item>
+    <item msgid="5667906231066981731">"I-Bluetooth"</item>
+    <item msgid="346574747471703768">"I-Ethernet"</item>
+    <item msgid="5734728378097476003">"I-VPN"</item>
   </string-array>
-    <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"uhlobo olungaziwa lwenethiwekhi"</string>
+    <string name="network_switch_type_name_unknown" msgid="5116448402191972082">"uhlobo olungaziwa lwenethiwekhi"</string>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 1af00c7..81782f9 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -176,7 +176,7 @@
     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 destroyAndAwaitReplacement in order
+    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/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 06a4cef..c7223fc 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -105,5 +105,19 @@
 # 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
+
+# From nearby-service, including proto
+rule service.proto.** com.android.server.nearby.@0
+rule androidx.annotation.Keep* com.android.server.nearby.@0
+rule androidx.collection.** com.android.server.nearby.@0
+rule androidx.core.** com.android.server.nearby.@0
+rule androidx.versionedparcelable.** com.android.server.nearby.@0
+rule com.google.common.** com.android.server.nearby.@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 f13c68d..bc70c93 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -39,148 +39,126 @@
 
 namespace android {
 
-static void native_init(JNIEnv* env, jobject clazz) {
+#define CHECK_LOG(status) \
+  do { \
+    if (!isOk(status)) \
+      ALOGE("%s failed, error code = %d", __func__, status.code()); \
+  } while (0)
+
+static void native_init(JNIEnv* env, jclass clazz) {
   Status status = mTc.start();
-   if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
 }
 
-static jint native_addNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_addNaughtyApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
       TrafficController::IptOp::IptOpInsert);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_removeNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_removeNaughtyApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
       TrafficController::IptOp::IptOpDelete);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_addNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_addNiceApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
       TrafficController::IptOp::IptOpInsert);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_removeNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_removeNiceApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
       TrafficController::IptOp::IptOpDelete);
-  if (!isOk(status)) {
-    ALOGD("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_setChildChain(JNIEnv* env, jobject clazz, jint childChain, jboolean enable) {
+static jint native_setChildChain(JNIEnv* env, jobject self, jint childChain, jboolean enable) {
   auto chain = static_cast<ChildChain>(childChain);
   int res = mTc.toggleUidOwnerMap(chain, enable);
-  if (res) {
-    ALOGE("%s failed, error code = %d", __func__, res);
-  }
+  if (res) ALOGE("%s failed, error code = %d", __func__, res);
   return (jint)res;
 }
 
-static jint native_replaceUidChain(JNIEnv* env, jobject clazz, jstring name, jboolean isAllowlist,
-                                jintArray jUids) {
+static jint native_replaceUidChain(JNIEnv* env, jobject self, jstring name, jboolean isAllowlist,
+                                   jintArray jUids) {
     const ScopedUtfChars chainNameUtf8(env, name);
-    if (chainNameUtf8.c_str() == nullptr) {
-        return -EINVAL;
-    }
+    if (chainNameUtf8.c_str() == nullptr) return -EINVAL;
     const std::string chainName(chainNameUtf8.c_str());
 
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     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) {
-      ALOGE("%s failed, error code = %d", __func__, res);
-    }
+    if (res) ALOGE("%s failed, error code = %d", __func__, res);
     return (jint)res;
 }
 
-static jint native_setUidRule(JNIEnv* env, jobject clazz, jint childChain, jint uid,
-                          jint firewallRule) {
+static jint native_setUidRule(JNIEnv* env, jobject self, jint childChain, jint uid,
+                              jint firewallRule) {
     auto chain = static_cast<ChildChain>(childChain);
     auto rule = static_cast<FirewallRule>(firewallRule);
     FirewallType fType = mTc.getFirewallType(chain);
 
     int res = mTc.changeUidOwnerRule(chain, uid, rule, fType);
-    if (res) {
-      ALOGE("%s failed, error code = %d", __func__, res);
-    }
+    if (res) ALOGE("%s failed, error code = %d", __func__, res);
     return (jint)res;
 }
 
-static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName,
-                                    jintArray jUids) {
-    const ScopedUtfChars ifNameUtf8(env, ifName);
-    if (ifNameUtf8.c_str() == nullptr) {
-        return -EINVAL;
+static jint native_addUidInterfaceRules(JNIEnv* env, jobject self, jstring ifName,
+                                        jintArray jUids) {
+    // Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex is
+    // set to 0.
+    int ifIndex = 0;
+    if (ifName != nullptr) {
+        const ScopedUtfChars ifNameUtf8(env, ifName);
+        const std::string interfaceName(ifNameUtf8.c_str());
+        ifIndex = if_nametoindex(interfaceName.c_str());
     }
-    const std::string interfaceName(ifNameUtf8.c_str());
-    const int ifIndex = if_nametoindex(interfaceName.c_str());
 
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     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)) {
-        ALOGE("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static jint native_removeUidInterfaceRules(JNIEnv* env, jobject clazz, jintArray jUids) {
+static jint native_removeUidInterfaceRules(JNIEnv* env, jobject self, jintArray jUids) {
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     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)) {
-        ALOGE("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static jint native_swapActiveStatsMap(JNIEnv* env, jobject clazz) {
+static jint native_swapActiveStatsMap(JNIEnv* env, jobject self) {
     Status status = mTc.swapActiveStatsMap();
-    if (!isOk(status)) {
-        ALOGD("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static void native_setPermissionForUids(JNIEnv* env, jobject clazz, jint permission,
-                                      jintArray jUids) {
+static void native_setPermissionForUids(JNIEnv* env, jobject self, jint permission,
+                                        jintArray jUids) {
     ScopedIntArrayRO uids(env, jUids);
     if (uids.get() == nullptr) return;
 
@@ -190,7 +168,7 @@
     mTc.setPermissionForUids(permission, data);
 }
 
-static void native_dump(JNIEnv* env, jobject clazz, jobject javaFd, jboolean verbose) {
+static void native_dump(JNIEnv* env, jobject self, jobject javaFd, jboolean verbose) {
     int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (fd < 0) {
         jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
@@ -235,9 +213,8 @@
 // clang-format on
 
 int register_com_android_server_BpfNetMaps(JNIEnv* env) {
-    return jniRegisterNativeMethods(env,
-    "com/android/server/BpfNetMaps",
-    gMethods, NELEM(gMethods));
+    return jniRegisterNativeMethods(env, "com/android/server/BpfNetMaps",
+                                    gMethods, NELEM(gMethods));
 }
 
 }; // namespace android
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 4517b5c..e2c5a63 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -34,7 +34,6 @@
 #include <netjniutils/netjniutils.h>
 #include <private/android_filesystem_config.h>
 
-#include "libclat/bpfhelper.h"
 #include "libclat/clatutils.h"
 #include "nativehelper/scoped_utf_chars.h"
 
@@ -257,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) {
@@ -355,7 +314,8 @@
     }
 
     // TODO: use android::base::ScopeGuard.
-    if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)) {
+    if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK
+                                           | POSIX_SPAWN_CLOEXEC_DEFAULT)) {
         posix_spawnattr_destroy(&attr);
         throwIOException(env, "posix_spawnattr_setflags failed", ret);
         return -1;
@@ -404,15 +364,6 @@
     posix_spawnattr_destroy(&attr);
     posix_spawn_file_actions_destroy(&fa);
 
-    // 6. 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;
 }
 
@@ -467,14 +418,6 @@
         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);
 }
 
diff --git a/service/native/Android.bp b/service/native/Android.bp
index cb26bc3..697fcbd 100644
--- a/service/native/Android.bp
+++ b/service/native/Android.bp
@@ -52,7 +52,8 @@
 
 cc_test {
     name: "traffic_controller_unit_test",
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
     require_root: true,
     local_include_dirs: ["include"],
     header_libs: [
@@ -71,4 +72,13 @@
         "libnetd_updatable",
         "netd_aidl_interface-lateststable-ndk",
     ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
 }
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 3e98edb..68fc9c8 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -74,6 +74,8 @@
 const char* TrafficController::LOCAL_POWERSAVE = "fw_powersave";
 const char* TrafficController::LOCAL_RESTRICTED = "fw_restricted";
 const char* TrafficController::LOCAL_LOW_POWER_STANDBY = "fw_low_power_standby";
+const char* TrafficController::LOCAL_OEM_DENY_1 = "fw_oem_deny_1";
+const char* TrafficController::LOCAL_OEM_DENY_2 = "fw_oem_deny_2";
 
 static_assert(BPF_PERMISSION_INTERNET == INetd::PERMISSION_INTERNET,
               "Mismatch between BPF and AIDL permissions: PERMISSION_INTERNET");
@@ -88,7 +90,7 @@
         }                                   \
     } while (0)
 
-const std::string uidMatchTypeToString(uint8_t match) {
+const std::string uidMatchTypeToString(uint32_t match) {
     std::string matchType;
     FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
     FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
@@ -98,6 +100,9 @@
     FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
     FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
     FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
+    FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
+    FLAG_MSG_TRANS(matchType, OEM_DENY_1_MATCH, match);
+    FLAG_MSG_TRANS(matchType, OEM_DENY_2_MATCH, match);
     if (match) {
         return StringPrintf("Unknown match: %u", match);
     }
@@ -183,6 +188,7 @@
     RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
     RETURN_IF_NOT_OK(mUidOwnerMap.clear());
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+    ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
 }
@@ -272,7 +278,7 @@
     if (oldMatch.ok()) {
         UidOwnerValue newMatch = {
                 .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
-                .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+                .rule = oldMatch.value().rule & ~match,
         };
         if (newMatch.rule == 0) {
             RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
@@ -286,23 +292,20 @@
 }
 
 Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
-    // iif should be non-zero if and only if match == MATCH_IIF
-    if (match == IIF_MATCH && iif == 0) {
-        return statusFromErrno(EINVAL, "Interface match must have nonzero interface index");
-    } else if (match != IIF_MATCH && iif != 0) {
+    if (match != IIF_MATCH && iif != 0) {
         return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
     }
     auto oldMatch = mUidOwnerMap.readValue(uid);
     if (oldMatch.ok()) {
         UidOwnerValue newMatch = {
-                .iif = iif ? iif : oldMatch.value().iif,
-                .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+                .iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
+                .rule = oldMatch.value().rule | match,
         };
         RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
     } else {
         UidOwnerValue newMatch = {
                 .iif = iif,
-                .rule = static_cast<uint8_t>(match),
+                .rule = match,
         };
         RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
     }
@@ -335,6 +338,12 @@
             return ALLOWLIST;
         case LOW_POWER_STANDBY:
             return ALLOWLIST;
+        case LOCKDOWN:
+            return DENYLIST;
+        case OEM_DENY_1:
+            return DENYLIST;
+        case OEM_DENY_2:
+            return DENYLIST;
         case NONE:
         default:
             return DENYLIST;
@@ -360,6 +369,15 @@
         case LOW_POWER_STANDBY:
             res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
             break;
+        case LOCKDOWN:
+            res = updateOwnerMapEntry(LOCKDOWN_VPN_MATCH, uid, rule, type);
+            break;
+        case OEM_DENY_1:
+            res = updateOwnerMapEntry(OEM_DENY_1_MATCH, uid, rule, type);
+            break;
+        case OEM_DENY_2:
+            res = updateOwnerMapEntry(OEM_DENY_2_MATCH, uid, rule, type);
+            break;
         case NONE:
         default:
             ALOGW("Unknown child chain: %d", chain);
@@ -399,9 +417,6 @@
 
 Status TrafficController::addUidInterfaceRules(const int iif,
                                                const std::vector<int32_t>& uidsToAdd) {
-    if (!iif) {
-        return statusFromErrno(EINVAL, "Interface rule must specify interface");
-    }
     std::lock_guard guard(mMutex);
 
     for (auto uid : uidsToAdd) {
@@ -440,6 +455,10 @@
         res = replaceRulesInMap(RESTRICTED_MATCH, uids);
     } else if (!name.compare(LOCAL_LOW_POWER_STANDBY)) {
         res = replaceRulesInMap(LOW_POWER_STANDBY_MATCH, uids);
+    } else if (!name.compare(LOCAL_OEM_DENY_1)) {
+        res = replaceRulesInMap(OEM_DENY_1_MATCH, uids);
+    } else if (!name.compare(LOCAL_OEM_DENY_2)) {
+        res = replaceRulesInMap(OEM_DENY_2_MATCH, uids);
     } else {
         ALOGE("unknown chain name: %s", name.c_str());
         return -EINVAL;
@@ -454,15 +473,15 @@
 int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
     std::lock_guard guard(mMutex);
     uint32_t key = UID_RULES_CONFIGURATION_KEY;
-    auto oldConfiguration = mConfigurationMap.readValue(key);
-    if (!oldConfiguration.ok()) {
+    auto oldConfigure = mConfigurationMap.readValue(key);
+    if (!oldConfigure.ok()) {
         ALOGE("Cannot read the old configuration from map: %s",
-              oldConfiguration.error().message().c_str());
-        return -oldConfiguration.error().code();
+              oldConfigure.error().message().c_str());
+        return -oldConfigure.error().code();
     }
     Status res;
     BpfConfig newConfiguration;
-    uint8_t match;
+    uint32_t match;
     switch (chain) {
         case DOZABLE:
             match = DOZABLE_MATCH;
@@ -479,11 +498,17 @@
         case LOW_POWER_STANDBY:
             match = LOW_POWER_STANDBY_MATCH;
             break;
+        case OEM_DENY_1:
+            match = OEM_DENY_1_MATCH;
+            break;
+        case OEM_DENY_2:
+            match = OEM_DENY_2_MATCH;
+            break;
         default:
             return -EINVAL;
     }
     newConfiguration =
-            enable ? (oldConfiguration.value() | match) : (oldConfiguration.value() & (~match));
+            enable ? (oldConfigure.value() | match) : (oldConfigure.value() & (~match));
     res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
     if (!isOk(res)) {
         ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
@@ -495,17 +520,17 @@
     std::lock_guard guard(mMutex);
 
     uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
-    auto oldConfiguration = mConfigurationMap.readValue(key);
-    if (!oldConfiguration.ok()) {
+    auto oldConfigure = mConfigurationMap.readValue(key);
+    if (!oldConfigure.ok()) {
         ALOGE("Cannot read the old configuration from map: %s",
-              oldConfiguration.error().message().c_str());
-        return Status(oldConfiguration.error().code(), oldConfiguration.error().message());
+              oldConfigure.error().message().c_str());
+        return Status(oldConfigure.error().code(), oldConfigure.error().message());
     }
 
     // Write to the configuration map to inform the kernel eBPF program to switch
     // from using one map to the other. Use flag BPF_EXIST here since the map should
     // be already populated in initMaps.
-    uint8_t newConfigure = (oldConfiguration.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
+    uint32_t newConfigure = (oldConfigure.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
     auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure,
                                             BPF_EXIST);
     if (!res.ok()) {
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 9529cae..dfa7097 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -36,6 +36,7 @@
 
 #include <netdutils/MockSyscalls.h>
 
+#define TEST_BPF_MAP
 #include "TrafficController.h"
 #include "bpf/BpfUtils.h"
 #include "NetdUpdatablePublic.h"
@@ -65,7 +66,7 @@
     BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
     BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
     BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
-    BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+    BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
     BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
 
@@ -73,52 +74,42 @@
         std::lock_guard guard(mTc.mMutex);
         ASSERT_EQ(0, setrlimitForTest());
 
-        mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
-                                          TEST_MAP_SIZE, 0));
+        mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeCookieTagMap);
 
-        mFakeAppUidStatsMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(StatsValue),
-                                            TEST_MAP_SIZE, 0));
+        mFakeAppUidStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeAppUidStatsMap);
 
-        mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
-                                       TEST_MAP_SIZE, 0));
+        mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeStatsMapA);
 
-        mFakeConfigurationMap.reset(
-                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_HASH, 1);
         ASSERT_VALID(mFakeConfigurationMap);
 
-        mFakeUidOwnerMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(UidOwnerValue),
-                                         TEST_MAP_SIZE, 0));
+        mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeUidOwnerMap);
-        mFakeUidPermissionMap.reset(
-                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeUidPermissionMap);
 
-        mTc.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+        mTc.mCookieTagMap = mFakeCookieTagMap;
         ASSERT_VALID(mTc.mCookieTagMap);
-        mTc.mAppUidStatsMap.reset(dupFd(mFakeAppUidStatsMap.getMap()));
+        mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
         ASSERT_VALID(mTc.mAppUidStatsMap);
-        mTc.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+        mTc.mStatsMapA = mFakeStatsMapA;
         ASSERT_VALID(mTc.mStatsMapA);
-        mTc.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+        mTc.mConfigurationMap = mFakeConfigurationMap;
         ASSERT_VALID(mTc.mConfigurationMap);
 
         // Always write to stats map A by default.
         ASSERT_RESULT_OK(mTc.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
                                                           SELECT_MAP_A, BPF_ANY));
-        mTc.mUidOwnerMap.reset(dupFd(mFakeUidOwnerMap.getMap()));
+        mTc.mUidOwnerMap = mFakeUidOwnerMap;
         ASSERT_VALID(mTc.mUidOwnerMap);
-        mTc.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
+        mTc.mUidPermissionMap = mFakeUidPermissionMap;
         ASSERT_VALID(mTc.mUidPermissionMap);
         mTc.mPrivilegedUser.clear();
     }
 
-    int dupFd(const android::base::unique_fd& mapFd) {
-        return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
-    }
-
     void populateFakeStats(uint64_t cookie, uint32_t uid, uint32_t tag, StatsKey* key) {
         UidTagValue cookieMapkey = {.uid = (uint32_t)uid, .tag = tag};
         EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, cookieMapkey, BPF_ANY));
@@ -307,6 +298,9 @@
     checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
     checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
     checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
+    checkUidOwnerRuleForChain(LOCKDOWN, LOCKDOWN_VPN_MATCH);
+    checkUidOwnerRuleForChain(OEM_DENY_1, OEM_DENY_1_MATCH);
+    checkUidOwnerRuleForChain(OEM_DENY_2, OEM_DENY_2_MATCH);
     ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
     ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
 }
@@ -318,6 +312,8 @@
     checkUidMapReplace("fw_powersave", uids, POWERSAVE_MATCH);
     checkUidMapReplace("fw_restricted", uids, RESTRICTED_MATCH);
     checkUidMapReplace("fw_low_power_standby", uids, LOW_POWER_STANDBY_MATCH);
+    checkUidMapReplace("fw_oem_deny_1", uids, OEM_DENY_1_MATCH);
+    checkUidMapReplace("fw_oem_deny_2", uids, OEM_DENY_2_MATCH);
     ASSERT_EQ(-EINVAL, mTc.replaceUidOwnerMap("unknow", true, uids));
 }
 
@@ -491,6 +487,70 @@
     checkEachUidValue({10001, 10002}, IIF_MATCH);
 }
 
+TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to uids
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+}
+
+TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to two uids
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+
+    // Remove interface rule from one of the uids
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+    expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
+    checkEachUidValue({1001}, IIF_MATCH);
+
+    // Remove interface rule from the remaining uid
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
+    // Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpInsert)));
+
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to the existing uid
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+    // Remove interface rule with wildcard from the existing uid
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Set up existing interface rule with wildcard
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+
+    // Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+    // Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpDelete)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpDelete)));
+    expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
+}
+
 TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
     std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
 
@@ -608,7 +668,7 @@
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
 
     void SetUp() {
-        mCookieTagMap.reset(android::bpf::mapRetrieveRW(COOKIE_TAG_MAP_PATH));
+        mCookieTagMap.init(COOKIE_TAG_MAP_PATH);
         ASSERT_TRUE(mCookieTagMap.isValid());
     }
 
@@ -620,7 +680,7 @@
                 if (res.ok() || (res.error().code() == ENOENT)) {
                     return Result<void>();
                 }
-                ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s\n", key,
+                ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s", key,
                       strerror(res.error().code()));
             }
             // Move forward to next cookie in the map.
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
index dc44845..48f68ea 100644
--- a/service/native/include/Common.h
+++ b/service/native/include/Common.h
@@ -35,6 +35,9 @@
     POWERSAVE = 3,
     RESTRICTED = 4,
     LOW_POWER_STANDBY = 5,
+    LOCKDOWN = 6,
+    OEM_DENY_1 = 7,
+    OEM_DENY_2 = 8,
     INVALID_CHAIN
 };
 // LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index 79e75ac..7a36e1e 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -88,6 +88,8 @@
     static const char* LOCAL_POWERSAVE;
     static const char* LOCAL_RESTRICTED;
     static const char* LOCAL_LOW_POWER_STANDBY;
+    static const char* LOCAL_OEM_DENY_1;
+    static const char* LOCAL_OEM_DENY_2;
 
   private:
     /*
@@ -149,13 +151,13 @@
      * the map right now:
      * - Entry with UID_RULES_CONFIGURATION_KEY:
      *    Store the configuration for the current uid rules. It indicates the device
-     *    is in doze/powersave/standby/restricted/low power standby mode.
+     *    is in doze/powersave/standby/restricted/low power standby/oem deny mode.
      * - Entry with CURRENT_STATS_MAP_CONFIGURATION_KEY:
      *    Stores the current live stats map that kernel program is writing to.
      *    Userspace can do scraping and cleaning job on the other one depending on the
      *    current configs.
      */
-    bpf::BpfMap<uint32_t, uint8_t> mConfigurationMap GUARDED_BY(mMutex);
+    bpf::BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
 
     /*
      * mUidOwnerMap: Store uids that are used for bandwidth control uid match.
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 17ee996..54d40ac 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"],
@@ -42,12 +35,9 @@
 cc_test {
     name: "libclat_test",
     defaults: ["netd_defaults"],
-    test_suites: ["device-tests"],
-    header_libs: [
-        "bpf_connectivity_headers",
-    ],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
     srcs: [
-        "TcUtilsTest.cpp",
         "clatutils_test.cpp",
     ],
     static_libs: [
@@ -55,12 +45,19 @@
         "libclat",
         "libip_checksum",
         "libnetd_test_tun_interface",
-        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
-        "libtcutils",
     ],
     shared_libs: [
         "liblog",
         "libnetutils",
     ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
     require_root: true,
 }
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 00785ad..0000000
--- a/service/native/libs/libclat/bpfhelper.cpp
+++ /dev/null
@@ -1,221 +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::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/proguard.flags b/service/proguard.flags
new file mode 100644
index 0000000..94397ab
--- /dev/null
+++ b/service/proguard.flags
@@ -0,0 +1,18 @@
+# Make sure proguard keeps all connectivity classes
+# TODO: instead of keeping everything, consider listing only "entry points"
+# (service loader, JNI registered methods, etc) and letting the optimizer do its job
+-keep class android.net.** { *; }
+-keep class com.android.connectivity.** { *; }
+-keep class com.android.net.** { *; }
+-keep class !com.android.server.nearby.**,com.android.server.** { *; }
+
+# Prevent proguard from stripping out any nearby-service and fast-pair-lite-protos fields.
+-keep class com.android.server.nearby.NearbyService { *; }
+-keep class com.android.server.nearby.service.proto { *; }
+
+# The lite proto runtime uses reflection to access fields based on the names in
+# the schema, keep all the fields.
+# This replicates the base proguard rule used by the build by default
+# (proguard_basic_keeps.flags), but needs to be specified here because the
+# com.google.protobuf package is jarjared to the below package.
+-keepclassmembers class * extends com.android.connectivity.com.google.protobuf.MessageLite { <fields>; }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index e58160a..8d8442f 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;
@@ -105,6 +108,7 @@
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
 import android.app.usage.NetworkStatsManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -255,6 +259,7 @@
 import com.android.net.module.util.netlink.InetDiagMessage;
 import com.android.server.connectivity.AutodestructReference;
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
+import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.DnsManager;
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
@@ -605,13 +610,6 @@
     // Handle private DNS validation status updates.
     private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;
 
-    /**
-     * used to remove a network request, either a listener or a real request and call unavailable
-     * arg1 = UID of caller
-     * obj  = NetworkRequest
-     */
-    private static final int EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE = 39;
-
      /**
       * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has
       * been tested.
@@ -750,7 +748,7 @@
      * The BPF program attached to the tc-police hook to account for to-be-dropped traffic.
      */
     private static final String TC_POLICE_BPF_PROG_PATH =
-            "/sys/fs/bpf/prog_netd_schedact_ingress_account";
+            "/sys/fs/bpf/netd_shared/prog_netd_schedact_ingress_account";
 
     private static String eventName(int what) {
         return sMagicDecoderRing.get(what, Integer.toString(what));
@@ -1189,6 +1187,7 @@
     /**
      * Keeps track of the number of requests made under different uids.
      */
+    // TODO: Remove the hack and use com.android.net.module.util.PerUidCounter instead.
     public static class PerUidCounter {
         private final int mMaxCountPerUid;
 
@@ -1402,6 +1401,19 @@
         }
 
         /**
+         * @see ClatCoordinator
+         */
+        public ClatCoordinator getClatCoordinator(INetd netd) {
+            return new ClatCoordinator(
+                new ClatCoordinator.Dependencies() {
+                    @NonNull
+                    public INetd getNetd() {
+                        return netd;
+                    }
+                });
+        }
+
+        /**
          * Wraps {@link TcUtils#tcFilterAddDevIngressPolice}
          */
         public void enableIngressRateLimit(String iface, long rateInBytesPerSecond) {
@@ -2235,6 +2247,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) {
@@ -2247,8 +2266,6 @@
         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());
@@ -2262,6 +2279,7 @@
             newNc.setAllowedUids(new ArraySet<>());
             newNc.setSubscriptionIds(Collections.emptySet());
         }
+        redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid);
 
         return newNc;
     }
@@ -2604,7 +2622,7 @@
         verifyCallingUidAndPackage(callingPackageName, mDeps.getCallingUid());
         enforceChangePermission(callingPackageName, callingAttributionTag);
         if (mProtectedNetworks.contains(networkType)) {
-            enforceConnectivityRestrictedNetworksPermission();
+            enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
         }
 
         InetAddress addr;
@@ -2855,12 +2873,16 @@
     }
 
     private void enforceNetworkFactoryPermission() {
+        // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
+        if (UserHandle.getAppId(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 (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.NETWORK_SETTINGS,
                 android.Manifest.permission.NETWORK_FACTORY,
@@ -2868,12 +2890,24 @@
     }
 
     private void enforceNetworkFactoryOrTestNetworksPermission() {
+        // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
+        if (UserHandle.getAppId(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)
+                || UserHandle.getAppId(uid) == Process.BLUETOOTH_UID;
+    }
+
     private boolean checkSettingsPermission() {
         return checkAnyPermissionOf(
                 android.Manifest.permission.NETWORK_SETTINGS,
@@ -2942,18 +2976,35 @@
                 android.Manifest.permission.NETWORK_SETTINGS);
     }
 
-    private void enforceConnectivityRestrictedNetworksPermission() {
-        try {
-            mContext.enforceCallingOrSelfPermission(
-                    android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,
-                    "ConnectivityService");
-            return;
-        } catch (SecurityException e) { /* fallback to ConnectivityInternalPermission */ }
-        //  TODO: Remove this fallback check after all apps have declared
-        //   CONNECTIVITY_USE_RESTRICTED_NETWORKS.
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CONNECTIVITY_INTERNAL,
-                "ConnectivityService");
+    private boolean checkConnectivityRestrictedNetworksPermission(int callingUid,
+            boolean checkUidsAllowedList) {
+        if (PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)) {
+            return true;
+        }
+
+        // fallback to ConnectivityInternalPermission
+        // TODO: Remove this fallback check after all apps have declared
+        //  CONNECTIVITY_USE_RESTRICTED_NETWORKS.
+        if (PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
+            return true;
+        }
+
+        // Check whether uid is in allowed on restricted networks list.
+        if (checkUidsAllowedList
+                && mPermissionMonitor.isUidAllowedOnRestrictedNetworks(callingUid)) {
+            return true;
+        }
+        return false;
+    }
+
+    private void enforceConnectivityRestrictedNetworksPermission(boolean checkUidsAllowedList) {
+        final int callingUid = mDeps.getCallingUid();
+        if (!checkConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
+            throw new SecurityException("ConnectivityService: user " + callingUid
+                    + " has no permission to access restricted network.");
+        }
     }
 
     private void enforceKeepalivePermission() {
@@ -3241,11 +3292,12 @@
             return;
         }
 
-        pw.print("NetworkProviders for:");
+        pw.println("NetworkProviders for:");
+        pw.increaseIndent();
         for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
-            pw.print(" " + npi.name);
+            pw.println(npi.providerId + ": " + npi.name);
         }
-        pw.println();
+        pw.decreaseIndent();
         pw.println();
 
         final NetworkAgentInfo defaultNai = getDefaultNetwork();
@@ -3294,6 +3346,14 @@
         pw.decreaseIndent();
         pw.println();
 
+        pw.println("Network Offers:");
+        pw.increaseIndent();
+        for (final NetworkOfferInfo offerInfo : mNetworkOffers) {
+            pw.println(offerInfo.offer);
+        }
+        pw.decreaseIndent();
+        pw.println();
+
         mLegacyTypeTracker.dump(pw);
 
         pw.println();
@@ -3368,6 +3428,10 @@
         for (NetworkAgentInfo nai : networksSortedById()) {
             pw.println(nai.toString());
             pw.increaseIndent();
+            pw.println("Nat464Xlat:");
+            pw.increaseIndent();
+            nai.dumpNat464Xlat(pw);
+            pw.decreaseIndent();
             pw.println(String.format(
                     "Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d",
                     nai.numForegroundNetworkRequests(),
@@ -3644,11 +3708,11 @@
                 }
                 case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: {
                     if (mDscpPolicyTracker != null) {
-                        mDscpPolicyTracker.removeAllDscpPolicies(nai);
+                        mDscpPolicyTracker.removeAllDscpPolicies(nai, true);
                     }
                     break;
                 }
-                case NetworkAgent.EVENT_DESTROY_AND_AWAIT_REPLACEMENT: {
+                case NetworkAgent.EVENT_UNREGISTER_AFTER_REPLACEMENT: {
                     // If nai is not yet created, or is already destroyed, ignore.
                     if (!shouldDestroyNativeNetwork(nai)) break;
 
@@ -3808,7 +3872,6 @@
             }
 
             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 =
@@ -4213,7 +4276,7 @@
     }
 
     private boolean shouldIgnoreValidationFailureAfterRoam(NetworkAgentInfo nai) {
-        // T+ devices should use destroyAndAwaitReplacement.
+        // T+ devices should use unregisterAfterReplacement.
         if (SdkLevel.isAtLeastT()) return false;
         final long blockTimeOut = Long.valueOf(mResources.get().getInteger(
                 R.integer.config_validationFailureAfterRoamIgnoreTimeMillis));
@@ -4385,6 +4448,9 @@
     }
 
     private void destroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
+        if (mDscpPolicyTracker != null) {
+            mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
+        }
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -4439,7 +4505,7 @@
 
     private boolean hasCarrierPrivilegeForNetworkCaps(final int callingUid,
             @NonNull final NetworkCapabilities caps) {
-        if (SdkLevel.isAtLeastT() && mCarrierPrivilegeAuthenticator != null) {
+        if (mCarrierPrivilegeAuthenticator != null) {
             return mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
                     callingUid, caps);
         }
@@ -4469,7 +4535,6 @@
 
     private void handleRegisterNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
         ensureRunningOnConnectivityServiceThread();
-        NetworkRequest requestToBeReleased = null;
         for (final NetworkRequestInfo nri : nris) {
             mNetworkRequestInfoLogs.log("REGISTER " + nri);
             checkNrisConsistency(nri);
@@ -4484,13 +4549,6 @@
                         }
                     }
                 }
-                if (req.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
-                    if (!hasCarrierPrivilegeForNetworkCaps(nri.mUid, req.networkCapabilities)
-                            && !checkConnectivityRestrictedNetworksPermission(
-                                    nri.mPid, nri.mUid)) {
-                        requestToBeReleased = req;
-                    }
-                }
             }
 
             // If this NRI has a satisfier already, it is replacing an older request that
@@ -4502,11 +4560,6 @@
             }
         }
 
-        if (requestToBeReleased != null) {
-            releaseNetworkRequestAndCallOnUnavailable(requestToBeReleased);
-            return;
-        }
-
         if (mFlags.noRematchAllRequestsOnRegister()) {
             rematchNetworksAndRequests(nris);
         } else {
@@ -5346,11 +5399,6 @@
                             /* callOnUnavailable */ false);
                     break;
                 }
-                case EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE: {
-                    handleReleaseNetworkRequest((NetworkRequest) msg.obj, msg.arg1,
-                            /* callOnUnavailable */ true);
-                    break;
-                }
                 case EVENT_SET_ACCEPT_UNVALIDATED: {
                     Network network = (Network) msg.obj;
                     handleSetAcceptUnvalidated(network, toBool(msg.arg1), toBool(msg.arg2));
@@ -5920,6 +5968,10 @@
                     + Arrays.toString(ranges) + "): netd command failed: " + e);
         }
 
+        if (SdkLevel.isAtLeastT()) {
+            mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
+        }
+
         for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
             final boolean curMetered = nai.networkCapabilities.isMetered();
             maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
@@ -6569,7 +6621,7 @@
             case REQUEST:
                 networkCapabilities = new NetworkCapabilities(networkCapabilities);
                 enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
-                        callingAttributionTag);
+                        callingAttributionTag, callingUid);
                 // TODO: this is incorrect. We mark the request as metered or not depending on
                 //  the state of the app when the request is filed, but we never change the
                 //  request if the app changes network state. http://b/29964605
@@ -6659,26 +6711,19 @@
     }
 
     private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
-            String callingPackageName, String callingAttributionTag) {
+            String callingPackageName, String callingAttributionTag, final int callingUid) {
         if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
-            if (!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
-                enforceConnectivityRestrictedNetworksPermission();
+            // For T+ devices, callers with carrier privilege could request with CBS capabilities.
+            if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+                    && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities)) {
+                return;
             }
+            enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
         } else {
             enforceChangePermission(callingPackageName, callingAttributionTag);
         }
     }
 
-    private boolean checkConnectivityRestrictedNetworksPermission(int callerPid, int callerUid) {
-        if (checkAnyPermissionOf(callerPid, callerUid,
-                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)
-                || checkAnyPermissionOf(callerPid, callerUid,
-                android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
-            return true;
-        }
-        return false;
-    }
-
     @Override
     public boolean requestBandwidthUpdate(Network network) {
         enforceAccessPermission();
@@ -6737,7 +6782,7 @@
         final int callingUid = mDeps.getCallingUid();
         networkCapabilities = new NetworkCapabilities(networkCapabilities);
         enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
-                callingAttributionTag);
+                callingAttributionTag, callingUid);
         enforceMeteredApnPolicy(networkCapabilities);
         ensureRequestableCapabilities(networkCapabilities);
         ensureSufficientPermissionsForRequest(networkCapabilities,
@@ -6860,13 +6905,6 @@
                 EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
     }
 
-    private void releaseNetworkRequestAndCallOnUnavailable(NetworkRequest networkRequest) {
-        ensureNetworkRequestHasType(networkRequest);
-        mHandler.sendMessage(mHandler.obtainMessage(
-                EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE, mDeps.getCallingUid(), 0,
-                networkRequest));
-    }
-
     private void handleRegisterNetworkProvider(NetworkProviderInfo npi) {
         if (mNetworkProviderInfos.containsKey(npi.messenger)) {
             // Avoid creating duplicates. even if an app makes a direct AIDL call.
@@ -7697,10 +7735,10 @@
 
     private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp,
             NetworkAgentInfo nai) {
-        final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null;
-        final String newIface = newLp != null ? newLp.getInterfaceName() : null;
-        final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp);
-        final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp);
+        final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
+        final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
+        final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
+        final boolean needsFiltering = requiresVpnAllowRule(nai, newLp, newIface);
 
         if (!wasFiltering && !needsFiltering) {
             // Nothing to do.
@@ -7713,11 +7751,19 @@
         }
 
         final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
+        if (ranges == null || ranges.isEmpty()) {
+            return;
+        }
+
         final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
         // TODO: this create a window of opportunity for apps to receive traffic between the time
         // when the old rules are removed and the time when new rules are added. To fix this,
         // make eBPF support two allowlisted interfaces so here new rules can be added before the
         // old rules are being removed.
+
+        // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to receive
+        // packets on all interfaces. This is required to accept incoming traffic in Lockdown mode
+        // by overriding the Lockdown blocking rule.
         if (wasFiltering) {
             mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
         }
@@ -7778,6 +7824,7 @@
         }
         nai.declaredCapabilities = new NetworkCapabilities(nc);
         NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(nc, nai.creatorUid,
+                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE),
                 mCarrierPrivilegeAuthenticator);
     }
 
@@ -8006,15 +8053,14 @@
     }
 
     /**
-     * Returns whether VPN isolation (ingress interface filtering) should be applied on the given
-     * network.
+     * Returns the interface which requires VPN isolation (ingress interface filtering).
      *
      * Ingress interface filtering enforces that all apps under the given network can only receive
      * packets from the network's interface (and loopback). This is important for VPNs because
      * apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any
      * non-VPN interfaces.
      *
-     * As a result, this method should return true iff
+     * As a result, this method should return Non-null interface iff
      *  1. the network is an app VPN (not legacy VPN)
      *  2. the VPN does not allow bypass
      *  3. the VPN is fully-routed
@@ -8023,15 +8069,32 @@
      * @see INetd#firewallAddUidInterfaceRules
      * @see INetd#firewallRemoveUidInterfaceRules
      */
-    private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
+    @Nullable
+    private String getVpnIsolationInterface(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
             LinkProperties lp) {
-        if (nc == null || lp == null) return false;
-        return nai.isVPN()
+        if (nc == null || lp == null) return null;
+        if (nai.isVPN()
                 && !nai.networkAgentConfig.allowBypass
                 && nc.getOwnerUid() != Process.SYSTEM_UID
                 && lp.getInterfaceName() != null
                 && (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
-                && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute());
+                && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute())
+                && !lp.hasExcludeRoute()) {
+            return lp.getInterfaceName();
+        }
+        return null;
+    }
+
+    /**
+     * Returns whether we need to set interface filtering rule or not
+     */
+    private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
+            String filterIface) {
+        // Only filter if lp has an interface.
+        if (lp == null || lp.getInterfaceName() == null) return false;
+        // Before T, allow rules are only needed if VPN isolation is enabled.
+        // T and After T, allow rules are needed for all VPNs.
+        return filterIface != null || (nai.isVPN() && SdkLevel.isAtLeastT());
     }
 
     private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8159,9 +8222,10 @@
             if (!prevRanges.isEmpty()) {
                 updateVpnUidRanges(false, nai, prevRanges);
             }
-            final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
-            final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
-            final String iface = nai.linkProperties.getInterfaceName();
+            final String oldIface = getVpnIsolationInterface(nai, prevNc, nai.linkProperties);
+            final String newIface = getVpnIsolationInterface(nai, newNc, nai.linkProperties);
+            final boolean wasFiltering = requiresVpnAllowRule(nai, nai.linkProperties, oldIface);
+            final boolean shouldFilter = requiresVpnAllowRule(nai, nai.linkProperties, newIface);
             // For VPN uid interface filtering, old ranges need to be removed before new ranges can
             // be added, due to the range being expanded and stored as individual UIDs. For example
             // the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
@@ -8173,11 +8237,16 @@
             // above, where the addition of new ranges happens before the removal of old ranges.
             // TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
             // to be removed will never overlap with the new range to be added.
+
+            // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to
+            // receive packets on all interfaces. This is required to accept incoming traffic in
+            // Lockdown mode by overriding the Lockdown blocking rule.
             if (wasFiltering && !prevRanges.isEmpty()) {
-                mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
+                mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
+                        prevNc.getOwnerUid());
             }
             if (shouldFilter && !newRanges.isEmpty()) {
-                mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
+                mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
             }
         } catch (Exception e) {
             // Never crash!
@@ -9182,7 +9251,18 @@
             params.networkCapabilities = networkAgent.networkCapabilities;
             params.linkProperties = new LinkProperties(networkAgent.linkProperties,
                     true /* parcelSensitiveFields */);
-            networkAgent.networkMonitor().notifyNetworkConnected(params);
+            // isAtLeastT() is conservative here, as recent versions of NetworkStack support the
+            // newer callback even before T. However getInterfaceVersion is a synchronized binder
+            // call that would cause a Log.wtf to be emitted from the system_server process, and
+            // in the absence of a satisfactory, scalable solution which follows an easy/standard
+            // process to check the interface version, just use an SDK check. NetworkStack will
+            // always be new enough when running on T+.
+            if (SdkLevel.isAtLeastT()) {
+                networkAgent.networkMonitor().notifyNetworkConnected(params);
+            } else {
+                networkAgent.networkMonitor().notifyNetworkConnected(params.linkProperties,
+                        params.networkCapabilities);
+            }
             scheduleUnvalidatedPrompt(networkAgent);
 
             // Whether a particular NetworkRequest listen should cause signal strength thresholds to
@@ -10568,7 +10648,11 @@
         if (callback == null) throw new IllegalArgumentException("callback must be non-null");
 
         if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
-            enforceConnectivityRestrictedNetworksPermission();
+            // TODO: Check allowed list here and ensure that either a) any QoS callback registered
+            //  on this network is unregistered when the app loses permission or b) no QoS
+            //  callbacks are sent for restricted networks unless the app currently has permission
+            //  to access restricted networks.
+            enforceConnectivityRestrictedNetworksPermission(false /* checkUidsAllowedList */);
         }
         mQosCallbackTracker.registerCallback(callback, filter, nai);
     }
@@ -10584,13 +10668,29 @@
         mQosCallbackTracker.unregisterCallback(callback);
     }
 
+    private boolean isNetworkPreferenceAllowedForProfile(@NonNull UserHandle profile) {
+        // UserManager.isManagedProfile returns true for all apps in managed user profiles.
+        // Enterprise device can be fully managed like device owner and such use case
+        // also should be supported. Calling app check for work profile and fully managed device
+        // is already done in DevicePolicyManager.
+        // This check is an extra caution to be sure device is fully managed or not.
+        final UserManager um = mContext.getSystemService(UserManager.class);
+        final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+        if (um.isManagedProfile(profile.getIdentifier())) {
+            return true;
+        }
+        if (SdkLevel.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
+        return false;
+    }
+
     /**
-     * Request that a user profile is put by default on a network matching a given preference.
+     * Set a list of default network selection policies for a user profile or device owner.
      *
      * See the documentation for the individual preferences for a description of the supported
      * behaviors.
      *
-     * @param profile the user profile for whih the preference is being set.
+     * @param profile If the device owner is set, any profile is allowed.
+              Otherwise, the given profile can only be managed profile.
      * @param preferences the list of profile network preferences for the
      *        provided profile.
      * @param listener an optional listener to listen for completion of the operation.
@@ -10604,7 +10704,10 @@
         Objects.requireNonNull(profile);
 
         if (preferences.size() == 0) {
-            preferences.add((new ProfileNetworkPreference.Builder()).build());
+            final ProfileNetworkPreference pref = new ProfileNetworkPreference.Builder()
+                    .setPreference(ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT)
+                    .build();
+            preferences.add(pref);
         }
 
         PermissionUtils.enforceNetworkStackPermission(mContext);
@@ -10615,19 +10718,21 @@
             throw new IllegalArgumentException("Must explicitly specify a user handle ("
                     + "UserHandle.CURRENT not supported)");
         }
-        final UserManager um = mContext.getSystemService(UserManager.class);
-        if (!um.isManagedProfile(profile.getIdentifier())) {
-            throw new IllegalArgumentException("Profile must be a managed profile");
+        if (!isNetworkPreferenceAllowedForProfile(profile)) {
+            throw new IllegalArgumentException("Profile must be a managed profile "
+                    + "or the device owner must be set. ");
         }
 
         final List<ProfileNetworkPreferenceList.Preference> preferenceList =
                 new ArrayList<ProfileNetworkPreferenceList.Preference>();
-        boolean allowFallback = true;
+        boolean hasDefaultPreference = false;
         for (final ProfileNetworkPreference preference : preferences) {
             final NetworkCapabilities nc;
+            boolean allowFallback = true;
             switch (preference.getPreference()) {
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
                     nc = null;
+                    hasDefaultPreference = true;
                     if (preference.getPreferenceEnterpriseId() != 0) {
                         throw new IllegalArgumentException(
                                 "Invalid enterprise identifier in setProfileNetworkPreferences");
@@ -10637,6 +10742,14 @@
                     allowFallback = false;
                     // continue to process the enterprise preference.
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
+                    // This code is needed even though there is a check later on,
+                    // because isRangeAlreadyInPreferenceList assumes that every preference
+                    // has a UID list.
+                    if (hasDefaultPreference) {
+                        throw new IllegalArgumentException(
+                                "Default profile preference should not be set along with other "
+                                        + "preference");
+                    }
                     if (!isEnterpriseIdentifierValid(preference.getPreferenceEnterpriseId())) {
                         throw new IllegalArgumentException(
                                 "Invalid enterprise identifier in setProfileNetworkPreferences");
@@ -10660,6 +10773,10 @@
             }
             preferenceList.add(new ProfileNetworkPreferenceList.Preference(
                     profile, nc, allowFallback));
+            if (hasDefaultPreference && preferenceList.size() > 1) {
+                throw new IllegalArgumentException(
+                        "Default profile preference should not be set along with other preference");
+            }
         }
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE,
                 new Pair<>(preferenceList, listener)));
@@ -10704,12 +10821,6 @@
         return false;
     }
 
-    private void validateNetworkCapabilitiesOfProfileNetworkPreference(
-            @Nullable final NetworkCapabilities nc) {
-        if (null == nc) return; // Null caps are always allowed. It means to remove the setting.
-        ensureRequestableCapabilities(nc);
-    }
-
     private ArraySet<NetworkRequestInfo> createNrisFromProfileNetworkPreferences(
             @NonNull final ProfileNetworkPreferenceList prefs) {
         final ArraySet<NetworkRequestInfo> result = new ArraySet<>();
@@ -10760,10 +10871,19 @@
     private void handleSetProfileNetworkPreference(
             @NonNull final List<ProfileNetworkPreferenceList.Preference> preferenceList,
             @Nullable final IOnCompleteListener listener) {
+        /*
+         * handleSetProfileNetworkPreference is always called for single user.
+         * preferenceList only contains preferences for different uids within the same user
+         * (enforced by getUidListToBeAppliedForNetworkPreference).
+         * Clear all the existing preferences for the user before applying new preferences.
+         *
+         */
+        mProfileNetworkPreferences = mProfileNetworkPreferences.withoutUser(
+                preferenceList.get(0).user);
         for (final ProfileNetworkPreferenceList.Preference preference : preferenceList) {
-            validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities);
             mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference);
         }
+
         removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_PROFILE);
         addPerAppDefaultNetworkRequests(
                 createNrisFromProfileNetworkPreferences(mProfileNetworkPreferences));
@@ -11218,17 +11338,45 @@
     }
 
     @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);
+            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:
+            case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1:
+            case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2:
+                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();
@@ -11262,6 +11410,12 @@
                     mBpfNetMaps.replaceUidChain("fw_low_power_standby", true /* isAllowList */,
                             uids);
                     break;
+                case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1:
+                    mBpfNetMaps.replaceUidChain("fw_oem_deny_1", false /* isAllowList */, uids);
+                    break;
+                case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2:
+                    mBpfNetMaps.replaceUidChain("fw_oem_deny_2", false /* isAllowList */, uids);
+                    break;
                 default:
                     throw new IllegalArgumentException("replaceFirewallChain with invalid chain: "
                             + chain);
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index a0bfb4a..e12190c 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import static android.net.TestNetworkManager.CLAT_INTERFACE_PREFIX;
 import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
 import static android.net.TestNetworkManager.TEST_TUN_PREFIX;
 
@@ -98,6 +99,14 @@
         }
     }
 
+    // TODO: find a way to allow the caller to pass in non-clat interface names, ensuring that
+    // those names do not conflict with names created by callers that do not pass in an interface
+    // name.
+    private static boolean isValidInterfaceName(@NonNull final String iface) {
+        return iface.startsWith(CLAT_INTERFACE_PREFIX + TEST_TUN_PREFIX)
+                || iface.startsWith(CLAT_INTERFACE_PREFIX + TEST_TAP_PREFIX);
+    }
+
     /**
      * Create a TUN or TAP interface with the specified parameters.
      *
@@ -106,29 +115,35 @@
      */
     @Override
     public TestNetworkInterface createInterface(boolean isTun, boolean bringUp,
-            LinkAddress[] linkAddrs) {
+            LinkAddress[] linkAddrs, @Nullable String iface) {
         enforceTestNetworkPermissions(mContext);
 
         Objects.requireNonNull(linkAddrs, "missing linkAddrs");
 
-        String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
-        String iface = ifacePrefix + sTestTunIndex.getAndIncrement();
+        String interfaceName = iface;
+        if (iface == null) {
+            String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
+            interfaceName = ifacePrefix + sTestTunIndex.getAndIncrement();
+        } else if (!isValidInterfaceName(iface)) {
+            throw new IllegalArgumentException("invalid interface name requested: " + iface);
+        }
+
         final long token = Binder.clearCallingIdentity();
         try {
             ParcelFileDescriptor tunIntf =
-                    ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, iface));
+                    ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, interfaceName));
             for (LinkAddress addr : linkAddrs) {
                 mNetd.interfaceAddAddress(
-                        iface,
+                        interfaceName,
                         addr.getAddress().getHostAddress(),
                         addr.getPrefixLength());
             }
 
             if (bringUp) {
-                NetdUtils.setInterfaceUp(mNetd, iface);
+                NetdUtils.setInterfaceUp(mNetd, interfaceName);
             }
 
-            return new TestNetworkInterface(tunIntf, iface);
+            return new TestNetworkInterface(tunIntf, interfaceName);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         } finally {
@@ -231,6 +246,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 c1a8195..8cefd47 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -18,26 +18,42 @@
 
 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.internal.util.IndentingPrintWriter;
+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,26 +82,53 @@
     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;
-    private static final long INVALID_COOKIE = 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/net_shared/map_clatd_clat_" + which + "_map";
+    }
+
+    private static String makeProgPath(boolean ingress, boolean ether) {
+        String path = "/sys/fs/bpf/net_shared/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 long mCookie = INVALID_COOKIE;
+    private ClatdTracker mClatdTracker = null;
 
+    /**
+     * Dependencies of ClatCoordinator which makes ConnectivityService injection
+     * in tests.
+     */
     @VisibleForTesting
-    abstract static class Dependencies {
+    public abstract static class Dependencies {
         /**
           * Get netd.
           */
@@ -201,9 +244,112 @@
         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)
@@ -227,6 +373,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;
+        }
     }
 
     /**
@@ -235,30 +504,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;
@@ -269,6 +554,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 */);
@@ -279,8 +570,14 @@
 
         // Detect ipv4 mtu.
         final Integer fwmark = getFwmark(netId);
-        final int detectedMtu = mDeps.detectMtu(pfx96,
+        final int detectedMtu;
+        try {
+            detectedMtu = mDeps.detectMtu(pfx96Str,
                 ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
+        } catch (IOException e) {
+            tunFd.close();
+            throw new IOException("Detect MTU on " + tunIface + " failed: " + e);
+        }
         final int mtu = adjustMtu(detectedMtu);
         Log.i(TAG, "ipv4 mtu is " + mtu);
 
@@ -295,7 +592,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};
@@ -333,8 +630,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();
@@ -343,7 +640,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();
@@ -352,7 +649,7 @@
         }
 
         // Tag socket as AID_CLAT to avoid duplicated CLAT data usage accounting.
-        long cookie;
+        final long cookie;
         try {
             cookie = mDeps.tagSocketAsClat(writeSock6.getFileDescriptor());
         } catch (IOException e) {
@@ -364,7 +661,7 @@
 
         // 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();
@@ -373,15 +670,12 @@
         }
 
         // [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;
-            mCookie = cookie;
+            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 {
@@ -390,29 +684,141 @@
             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);
-        mDeps.untagSocket(mCookie);
+        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;
-        mCookie = INVALID_COOKIE;
+    private void dumpBpfIngress(@NonNull IndentingPrintWriter pw) {
+        if (mIngressMap == null) {
+            pw.println("No BPF ingress6 map");
+            return;
+        }
+
+        try {
+            if (mIngressMap.isEmpty()) {
+                pw.println("<empty>");
+            }
+            pw.println("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif");
+            pw.increaseIndent();
+            mIngressMap.forEach((k, v) -> {
+                // TODO: print interface name
+                pw.println(String.format("%d %s/96 %s -> %s %d", k.iif, k.pfx96, k.local6,
+                        v.local4, v.oif));
+            });
+            pw.decreaseIndent();
+        } catch (ErrnoException e) {
+            pw.println("Error dumping BPF ingress6 map: " + e);
+        }
+    }
+
+    private void dumpBpfEgress(@NonNull IndentingPrintWriter pw) {
+        if (mEgressMap == null) {
+            pw.println("No BPF egress4 map");
+            return;
+        }
+
+        try {
+            if (mEgressMap.isEmpty()) {
+                pw.println("<empty>");
+            }
+            pw.println("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif");
+            pw.increaseIndent();
+            mEgressMap.forEach((k, v) -> {
+                // TODO: print interface name
+                pw.println(String.format("%d %s -> %s %s/96 %d %s", k.iif, k.local4, v.local6,
+                        v.pfx96, v.oif, v.oifIsEthernet != 0 ? "ether" : "rawip"));
+            });
+            pw.decreaseIndent();
+        } catch (ErrnoException e) {
+            pw.println("Error dumping BPF egress4 map: " + e);
+        }
+    }
+
+    /**
+     * Dump the cordinator information.
+     *
+     * @param pw print writer.
+     */
+    public void dump(@NonNull IndentingPrintWriter pw) {
+        // TODO: dump ClatdTracker
+        // TODO: move map dump to a global place to avoid duplicate dump while there are two or
+        // more IPv6 only networks.
+        pw.println("Forwarding rules:");
+        pw.increaseIndent();
+        dumpBpfIngress(pw);
+        dumpBpfEgress(pw);
+        pw.decreaseIndent();
+        pw.println();
+    }
+
+    /**
+     * Get clatd tracker. For test only.
+     */
+    @VisibleForTesting
+    @Nullable
+    ClatdTracker getClatdTrackerForTesting() {
+        return mClatdTracker;
     }
 
     private static native String native_selectIpv4Address(String v4addr, int prefixlen)
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..c1ba40e
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -0,0 +1,177 @@
+/*
+ * 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/net_shared/prog_block_bind4_block_port";
+    private static final String V6_PROG_PATH =
+            "/sys/fs/bpf/net_shared/prog_block_bind6_block_port";
+    private static final String BLOCKED_PORTS_MAP_PATH =
+            "/sys/fs/bpf/net_shared/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..7829d1a 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;
 
@@ -50,7 +52,7 @@
 
     private static final String TAG = DscpPolicyTracker.class.getSimpleName();
     private static final String PROG_PATH =
-            "/sys/fs/bpf/prog_dscp_policy_schedcls_set_dscp";
+            "/sys/fs/bpf/net_shared/prog_dscp_policy_schedcls_set_dscp";
     // Name is "map + *.o + map_name + map". Can probably shorten this
     private static final String IPV4_POLICY_MAP_PATH = makeMapPath(
             "dscp_policy_ipv4_dscp_policies");
@@ -59,37 +61,75 @@
     private static final int MAX_POLICIES = 16;
 
     private static String makeMapPath(String which) {
-        return "/sys/fs/bpf/map_" + which + "_map";
+        return "/sys/fs/bpf/net_shared/map_" + which + "_map";
     }
 
     private Set<String> mAttachedIfaces;
 
     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 799f46b..b13ba93 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -29,6 +29,7 @@
 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;
@@ -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;
@@ -126,7 +129,15 @@
     @VisibleForTesting
     static @NonNull String policyNameOf(final int policy) {
         final String name = sMessageNames.get(policy);
-        if (name == null) throw new IllegalArgumentException("Unknown policy: " + 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);
+        }
         return name.substring("POLICY_".length());
     }
 
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index 7b06682..e4ad391 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -17,6 +17,7 @@
 package com.android.server.connectivity;
 
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 
 import static com.android.net.module.util.CollectionUtils.contains;
 
@@ -36,9 +37,12 @@
 import android.util.Log;
 
 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.NetworkStackConstants;
 import com.android.server.ConnectivityService;
 
+import java.io.IOException;
 import java.net.Inet6Address;
 import java.util.Objects;
 
@@ -96,6 +100,7 @@
     private String mIface;
     private Inet6Address mIPv6Address;
     private State mState = State.IDLE;
+    private ClatCoordinator mClatCoordinator;
 
     private boolean mEnableClatOnCellular;
     private boolean mPrefixDiscoveryRunning;
@@ -106,6 +111,7 @@
         mNetd = netd;
         mNetwork = nai;
         mEnableClatOnCellular = deps.getCellular464XlatEnabled();
+        mClatCoordinator = deps.getClatCoordinator(mNetd);
     }
 
     /**
@@ -122,6 +128,11 @@
         final boolean supported = contains(NETWORK_TYPES, nai.networkInfo.getType());
         final boolean connected = contains(NETWORK_STATES, nai.networkInfo.getState());
 
+        // Allow to run clat on test network.
+        // TODO: merge to boolean "supported" once boolean "supported" is migrated to
+        // NetworkCapabilities.TRANSPORT_*.
+        final boolean isTestNetwork = nai.networkCapabilities.hasTransport(TRANSPORT_TEST);
+
         // Only run clat on networks that have a global IPv6 address and don't have a native IPv4
         // address.
         LinkProperties lp = nai.linkProperties;
@@ -132,8 +143,8 @@
         final boolean skip464xlat = (nai.netAgentConfig() != null)
                 && nai.netAgentConfig().skip464xlat;
 
-        return supported && connected && isIpv6OnlyNetwork && !skip464xlat && !nai.destroyed
-                && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
+        return (supported || isTestNetwork) && connected && isIpv6OnlyNetwork && !skip464xlat
+                && !nai.destroyed && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
                 ? isCellular464XlatEnabled() : true);
     }
 
@@ -179,10 +190,18 @@
     private void enterStartingState(String baseIface) {
         mNat64PrefixInUse = selectNat64Prefix();
         String addrStr = null;
-        try {
-            addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
-        } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+        if (SdkLevel.isAtLeastT()) {
+            try {
+                addrStr = mClatCoordinator.clatStart(baseIface, getNetId(), mNat64PrefixInUse);
+            } catch (IOException e) {
+                Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+            }
+        } else {
+            try {
+                addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
+            } catch (RemoteException | ServiceSpecificException e) {
+                Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+            }
         }
         mIface = CLAT_PREFIX + baseIface;
         mBaseIface = baseIface;
@@ -256,10 +275,18 @@
         }
 
         Log.i(TAG, "Stopping clatd on " + mBaseIface);
-        try {
-            mNetd.clatdStop(mBaseIface);
-        } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+        if (SdkLevel.isAtLeastT()) {
+            try {
+                mClatCoordinator.clatStop();
+            } catch (IOException e) {
+                Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+            }
+        } else {
+            try {
+                mNetd.clatdStop(mBaseIface);
+            } catch (RemoteException | ServiceSpecificException e) {
+                Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+            }
         }
 
         String iface = mIface;
@@ -506,6 +533,24 @@
         mNetwork.handler().post(() -> handleInterfaceRemoved(iface));
     }
 
+    /**
+     * Dump the NAT64 xlat information.
+     *
+     * @param pw print writer.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        if (SdkLevel.isAtLeastT()) {
+            if (isStarted()) {
+                pw.println("ClatCoordinator:");
+                pw.increaseIndent();
+                mClatCoordinator.dump(pw);
+                pw.decreaseIndent();
+            } else {
+                pw.println("<not started>");
+            }
+        }
+    }
+
     @Override
     public String toString() {
         return "mBaseIface: " + mBaseIface + ", mIface: " + mIface + ", mState: " + mState;
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index b73e2cc..b40b6e0 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -19,6 +19,7 @@
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.transportNamesOf;
 
@@ -59,6 +60,7 @@
 import android.util.Pair;
 import android.util.SparseArray;
 
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.WakeupMessage;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.server.ConnectivityService;
@@ -736,8 +738,8 @@
         }
 
         @Override
-        public void sendDestroyAndAwaitReplacement(final int timeoutMillis) {
-            mHandler.obtainMessage(NetworkAgent.EVENT_DESTROY_AND_AWAIT_REPLACEMENT,
+        public void sendUnregisterAfterReplacement(final int timeoutMillis) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_UNREGISTER_AFTER_REPLACEMENT,
                     new Pair<>(NetworkAgentInfo.this, timeoutMillis)).sendToTarget();
         }
     }
@@ -1186,6 +1188,15 @@
     }
 
     /**
+     * Dump the NAT64 xlat information.
+     *
+     * @param pw print writer.
+     */
+    public void dumpNat464Xlat(IndentingPrintWriter pw) {
+        clatd.dump(pw);
+    }
+
+    /**
      * Sets the most recent ConnectivityReport for this network.
      *
      * <p>This should only be called from the ConnectivityService thread.
@@ -1214,20 +1225,22 @@
      *
      * @param nc the capabilities to sanitize
      * @param creatorUid the UID of the process creating this network agent
+     * @param hasAutomotiveFeature true if this device has the automotive feature, false otherwise
      * @param authenticator the carrier privilege authenticator to check for telephony constraints
      */
     public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
-            final int creatorUid, @NonNull final CarrierPrivilegeAuthenticator authenticator) {
+            final int creatorUid, final boolean hasAutomotiveFeature,
+            @Nullable final CarrierPrivilegeAuthenticator authenticator) {
         if (nc.hasTransport(TRANSPORT_TEST)) {
             nc.restrictCapabilitiesForTestNetwork(creatorUid);
         }
-        if (!areAllowedUidsAcceptableFromNetworkAgent(nc, authenticator)) {
+        if (!areAllowedUidsAcceptableFromNetworkAgent(nc, hasAutomotiveFeature, authenticator)) {
             nc.setAllowedUids(new ArraySet<>());
         }
     }
 
     private static boolean areAllowedUidsAcceptableFromNetworkAgent(
-            @NonNull final NetworkCapabilities nc,
+            @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
             @Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
         // NCs without access UIDs are fine.
         if (!nc.hasAllowedUids()) return true;
@@ -1242,6 +1255,11 @@
         // access UIDs
         if (nc.hasTransport(TRANSPORT_TEST)) return true;
 
+        // Factories that make ethernet networks can allow UIDs for automotive devices.
+        if (nc.hasSingleTransport(TRANSPORT_ETHERNET) && hasAutomotiveFeature) {
+            return true;
+        }
+
         // Factories that make cell networks can allow the UID for the carrier service package.
         // This can only work in T where there is support for CarrierPrivilegeAuthenticator
         if (null != carrierPrivilegeAuthenticator
@@ -1252,8 +1270,6 @@
             return true;
         }
 
-        // TODO : accept Railway callers
-
         return false;
     }
 
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index 1e975dd..eea382e 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -22,6 +22,7 @@
 import android.net.NetworkRequest;
 import android.os.RemoteException;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
@@ -143,6 +144,11 @@
 
     @Override
     public String toString() {
-        return "NetworkOffer [ Score " + score + " Caps " + caps + "]";
+        final ArrayList<Integer> neededRequestIds = new ArrayList<>();
+        for (final NetworkRequest request : mCurrentlyNeeded) {
+            neededRequestIds.add(request.requestId);
+        }
+        return "NetworkOffer [ Provider Id (" + providerId + ") " + score + " Caps "
+                + caps + " Needed by " + neededRequestIds + "]";
     }
 }
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 2885ba7..e4a2c20 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -23,6 +23,9 @@
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -37,6 +40,7 @@
 import static com.android.net.module.util.CollectionUtils.toIntArray;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -74,7 +78,6 @@
 import com.android.server.BpfNetMaps;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -108,10 +111,19 @@
     @GuardedBy("this")
     private final SparseIntArray mUidToNetworkPerm = new SparseIntArray();
 
-    // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
-    // for apps under the VPN
+    // NonNull keys are active non-bypassable and fully-routed VPN's interface name, Values are uid
+    // ranges for apps under the VPNs which enable interface filtering.
+    // If key is null, Values are uid ranges for apps under the VPNs which are connected but do not
+    // enable interface filtering.
     @GuardedBy("this")
-    private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
+    private final Map<String, Set<UidRange>> mVpnInterfaceUidRanges = new ArrayMap<>();
+
+    // Items are uid ranges for apps under the VPN Lockdown
+    // Ranges were given through ConnectivityManager#setRequireVpnForUids, and ranges are allowed to
+    // have duplicates. Also, it is allowed to give ranges that are already subject to lockdown.
+    // So we need to maintain uid range with multiset.
+    @GuardedBy("this")
+    private final MultiSet<UidRange> mVpnLockdownUidRanges = new MultiSet<>();
 
     // A set of appIds for apps across all users on the device. We track appIds instead of uids
     // directly to reduce its size and also eliminate the need to update this set when user is
@@ -127,9 +139,19 @@
     @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. The permissions are a bitmask of PERMISSION_INTERNET and
+    // PERMISSION_UPDATE_DEVICE_STATS, or 0 (PERMISSION_NONE) if the app has neither of those
+    // permissions. They can never be PERMISSION_UNINSTALLED.
+    @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;
@@ -191,6 +213,38 @@
         }
     }
 
+    private static class MultiSet<T> {
+        private final Map<T, Integer> mMap = new ArrayMap<>();
+
+        /**
+         * Returns the number of key in the set before this addition.
+         */
+        public int add(T key) {
+            final int oldCount = mMap.getOrDefault(key, 0);
+            mMap.put(key, oldCount + 1);
+            return oldCount;
+        }
+
+        /**
+         * Return the number of key in the set before this removal.
+         */
+        public int remove(T key) {
+            final int oldCount = mMap.getOrDefault(key, 0);
+            if (oldCount == 0) {
+                Log.wtf(TAG, "Attempt to remove non existing key = " + key.toString());
+            } else if (oldCount == 1) {
+                mMap.remove(key);
+            } else {
+                mMap.put(key, oldCount - 1);
+            }
+            return oldCount;
+        }
+
+        public Set<T> getSet() {
+            return mMap.keySet();
+        }
+    }
+
     public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
             @NonNull final BpfNetMaps bpfNetMaps) {
         this(context, netd, bpfNetMaps, new Dependencies());
@@ -292,14 +346,24 @@
         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() {
@@ -363,6 +427,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) {
@@ -397,7 +465,14 @@
         if (appInfo == null) return false;
         // Check whether package's uid is in allowed on restricted networks uid list. If so, this
         // uid can have netd system permission.
-        return mUidsAllowedOnRestrictedNetworks.contains(appInfo.uid);
+        return isUidAllowedOnRestrictedNetworks(appInfo.uid);
+    }
+
+    /**
+     * Returns whether the given uid is in allowed on restricted networks list.
+     */
+    public synchronized boolean isUidAllowedOnRestrictedNetworks(final int uid) {
+        return mUidsAllowedOnRestrictedNetworks.contains(uid);
     }
 
     @VisibleForTesting
@@ -487,9 +562,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);
     }
 
     /**
@@ -502,6 +584,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++) {
@@ -512,6 +595,31 @@
             }
         }
         sendUidsNetworkPermission(removedUids, false /* add */);
+
+        // Remove appIds traffic permission that belongs to the user
+        final SparseIntArray removedUserAppIds = mUsersTrafficPermissions.remove(user);
+        // Generate appIds from the remaining users.
+        final SparseIntArray appIds = makeAppIdsTrafficPermForAllUsers();
+
+        if (removedUserAppIds == null) {
+            Log.wtf(TAG, "onUserRemoved: Receive unknown user=" + user);
+            return;
+        }
+
+        // Clear permission on those appIds belong to this user only, set the permission to
+        // PERMISSION_UNINSTALLED.
+        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);
     }
 
     /**
@@ -562,16 +670,26 @@
     }
 
     private synchronized void updateVpnUid(int uid, boolean add) {
-        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+        // Apps that can use restricted networks can always bypass VPNs.
+        if (hasRestrictedNetworksPermission(uid)) {
+            return;
+        }
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
             if (UidRange.containsUid(vpn.getValue(), uid)) {
                 final Set<Integer> changedUids = new HashSet<>();
                 changedUids.add(uid);
-                removeBypassingUids(changedUids, -1 /* vpnAppUid */);
                 updateVpnUidsInterfaceRules(vpn.getKey(), changedUids, add);
             }
         }
     }
 
+    private synchronized void updateLockdownUid(int uid, boolean add) {
+        if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)
+                && !hasRestrictedNetworksPermission(uid)) {
+            updateLockdownUidRule(uid, add);
+        }
+    }
+
     /**
      * This handles both network and traffic permission, because there is no overlap in actual
      * values, where network permission is NETWORK or SYSTEM, and traffic permission is INTERNET
@@ -598,6 +716,39 @@
         }
     }
 
+    private synchronized void updateAppIdTrafficPermission(int 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.
+        final int appId = UserHandle.getAppId(uid);
+        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.
      *
@@ -607,9 +758,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);
@@ -629,14 +783,17 @@
 
         // If the newly-installed package falls within some VPN's uid range, update Netd with it.
         // This needs to happen after the mUidToNetworkPerm update above, since
-        // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
-        // package can bypass VPN.
+        // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+        // mUidToNetworkPerm to check if the package can bypass VPN.
         updateVpnUid(uid, true /* add */);
+        updateLockdownUid(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) {
@@ -664,15 +821,19 @@
      * @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
-        // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
-        // package can bypass VPN.
+        // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+        // mUidToNetworkPerm to check if the package can bypass VPN.
         updateVpnUid(uid, false /* add */);
+        updateLockdownUid(uid, false /* add */);
         // If the package has been removed from all users on the device, clear it form mAllApps.
         if (mPackageManager.getNameForUid(uid) == null) {
             mAllApps.remove(appId);
@@ -680,10 +841,13 @@
 
         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;
@@ -751,48 +915,100 @@
     /**
      * Called when a new set of UID ranges are added to an active VPN network
      *
-     * @param iface The active VPN network's interface name
+     * @param iface The active VPN network's interface name. Null iface indicates that the app is
+     *              allowed to receive packets on all interfaces.
      * @param rangesToAdd The new UID ranges to be added to the network
      * @param vpnAppUid The uid of the VPN app
      */
-    public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
+    public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
             int vpnAppUid) {
         // Calculate the list of new app uids under the VPN due to the new UID ranges and update
         // Netd about them. Because mAllApps only contains appIds instead of uids, the result might
         // be an overestimation if an app is not installed on the user on which the VPN is running,
-        // but that's safe.
+        // but that's safe: if an app is not installed, it cannot receive any packets, so dropping
+        // packets to that UID is fine.
         final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
         removeBypassingUids(changedUids, vpnAppUid);
         updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
-        if (mVpnUidRanges.containsKey(iface)) {
-            mVpnUidRanges.get(iface).addAll(rangesToAdd);
+        if (mVpnInterfaceUidRanges.containsKey(iface)) {
+            mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
         } else {
-            mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
+            mVpnInterfaceUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
         }
     }
 
     /**
      * Called when a set of UID ranges are removed from an active VPN network
      *
-     * @param iface The VPN network's interface name
+     * @param iface The VPN network's interface name. Null iface indicates that the app is allowed
+     *              to receive packets on all interfaces.
      * @param rangesToRemove Existing UID ranges to be removed from the VPN network
      * @param vpnAppUid The uid of the VPN app
      */
-    public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
+    public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
             Set<UidRange> rangesToRemove, int vpnAppUid) {
         // Calculate the list of app uids that are no longer under the VPN due to the removed UID
         // ranges and update Netd about them.
         final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
         removeBypassingUids(changedUids, vpnAppUid);
         updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
-        Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
+        Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
         if (existingRanges == null) {
             loge("Attempt to remove unknown vpn uid Range iface = " + iface);
             return;
         }
         existingRanges.removeAll(rangesToRemove);
         if (existingRanges.size() == 0) {
-            mVpnUidRanges.remove(iface);
+            mVpnInterfaceUidRanges.remove(iface);
+        }
+    }
+
+    /**
+     * Called when UID ranges under VPN Lockdown are updated
+     *
+     * @param add {@code true} if the uids are to be added to the Lockdown, {@code false} if they
+     *        are to be removed from the Lockdown.
+     * @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
+     *               app's UID in any special way. The caller is responsible for excluding the VPN
+     *               app UID from the passed-in ranges.
+     *               Ranges can have duplications and/or contain the range that is already subject
+     *               to lockdown. However, ranges can not have overlaps with other ranges including
+     *               ranges that are currently subject to lockdown.
+     */
+    public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
+        final Set<UidRange> affectedUidRanges = new HashSet<>();
+
+        for (final UidRange range : ranges) {
+            if (add) {
+                // Rule will be added if mVpnLockdownUidRanges does not have this uid range entry
+                // currently.
+                if (mVpnLockdownUidRanges.add(range) == 0) {
+                    affectedUidRanges.add(range);
+                }
+            } else {
+                // Rule will be removed if the number of the range in the set is 1 before the
+                // removal.
+                if (mVpnLockdownUidRanges.remove(range) == 1) {
+                    affectedUidRanges.add(range);
+                }
+            }
+        }
+
+        // mAllApps only contains appIds instead of uids. So the generated uid list might contain
+        // apps that are installed only on some users but not others. But that's safe: if an app is
+        // not installed, it cannot receive any packets, so dropping packets to that UID is fine.
+        final Set<Integer> affectedUids = intersectUids(affectedUidRanges, mAllApps);
+
+        // We skip adding rule to privileged apps and allow them to bypass incoming packet
+        // filtering. The behaviour is consistent with how lockdown works for outgoing packets, but
+        // the implementation is different: while ConnectivityService#setRequireVpnForUids does not
+        // exclude privileged apps from the prohibit routing rules used to implement outgoing packet
+        // filtering, privileged apps can still bypass outgoing packet filtering because the
+        // prohibit rules observe the protected from VPN bit.
+        for (final int uid: affectedUids) {
+            if (!hasRestrictedNetworksPermission(uid)) {
+                updateLockdownUidRule(uid, add);
+            }
         }
     }
 
@@ -831,7 +1047,7 @@
      */
     private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
         uids.remove(vpnAppUid);
-        uids.removeIf(uid -> mUidToNetworkPerm.get(uid, PERMISSION_NONE) == PERMISSION_SYSTEM);
+        uids.removeIf(this::hasRestrictedNetworksPermission);
     }
 
     /**
@@ -840,6 +1056,7 @@
      *
      * This is to instruct netd to set up appropriate filtering rules for these uids, such that they
      * can only receive ingress packets from the VPN's tunnel interface (and loopback).
+     * Null iface set up a wildcard rule that allow app to receive packets on all interfaces.
      *
      * @param iface the interface name of the active VPN connection
      * @param add {@code true} if the uids are to be added to the interface, {@code false} if they
@@ -860,6 +1077,18 @@
         }
     }
 
+    private void updateLockdownUidRule(int uid, boolean add) {
+        try {
+            if (add) {
+                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_DENY);
+            } else {
+                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_ALLOW);
+            }
+        } catch (ServiceSpecificException e) {
+            loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
+        }
+    }
+
     /**
      * Send the updated permission information to netd. Called upon package install/uninstall.
      *
@@ -889,10 +1118,6 @@
      */
     @VisibleForTesting
     void sendAppIdsTrafficPermission(SparseIntArray netdPermissionsAppIds) {
-        if (mNetd == null) {
-            Log.e(TAG, "Failed to get the netd service");
-            return;
-        }
         final ArrayList<Integer> allPermissionAppIds = new ArrayList<>();
         final ArrayList<Integer> internetPermissionAppIds = new ArrayList<>();
         final ArrayList<Integer> updateStatsPermissionAppIds = new ArrayList<>();
@@ -951,8 +1176,14 @@
 
     /** Should only be used by unit tests */
     @VisibleForTesting
-    public Set<UidRange> getVpnUidRanges(String iface) {
-        return mVpnUidRanges.get(iface);
+    public Set<UidRange> getVpnInterfaceUidRanges(String iface) {
+        return mVpnInterfaceUidRanges.get(iface);
+    }
+
+    /** Should only be used by unit tests */
+    @VisibleForTesting
+    public Set<UidRange> getVpnLockdownUidRanges() {
+        return mVpnLockdownUidRanges.getSet();
     }
 
     private synchronized void onSettingChanged() {
@@ -1017,7 +1248,7 @@
     public void dump(IndentingPrintWriter pw) {
         pw.println("Interface filtering rules:");
         pw.increaseIndent();
-        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
             pw.println("Interface: " + vpn.getKey());
             pw.println("UIDs: " + vpn.getValue().toString());
             pw.println();
@@ -1025,6 +1256,14 @@
         pw.decreaseIndent();
 
         pw.println();
+        pw.println("Lockdown filtering rules:");
+        pw.increaseIndent();
+        for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
+            pw.println("UIDs: " + range.toString());
+        }
+        pw.decreaseIndent();
+
+        pw.println();
         pw.println("Update logs:");
         pw.increaseIndent();
         mPermissionUpdateLogs.reverseDump(pw);
diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
index 71f342d..5bafef9 100644
--- a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
+++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
@@ -70,23 +70,33 @@
     /**
      * Returns a new object consisting of this object plus the passed preference.
      *
-     * If a preference already exists for the same user, it will be replaced by the passed
-     * preference. Passing a Preference object containing a null capabilities object is equivalent
-     * to (and indeed, implemented as) removing the preference for this user.
+     * It is not expected that unwanted preference already exists for the same user.
+     * All preferences for the user that were previously configured should be cleared before
+     * adding a new preference.
+     * Passing a Preference object containing a null capabilities object is equivalent
+     * to removing the preference for this user.
      */
     public ProfileNetworkPreferenceList plus(@NonNull final Preference pref) {
-        final ArrayList<Preference> newPrefs = new ArrayList<>();
-        for (final Preference existingPref : preferences) {
-            if (!existingPref.user.equals(pref.user)) {
-                newPrefs.add(existingPref);
-            }
-        }
+        final ArrayList<Preference> newPrefs = new ArrayList<>(preferences);
         if (null != pref.capabilities) {
             newPrefs.add(pref);
         }
         return new ProfileNetworkPreferenceList(newPrefs);
     }
 
+    /**
+     * Remove all preferences corresponding to a user.
+     */
+    public ProfileNetworkPreferenceList withoutUser(UserHandle user) {
+        final ArrayList<Preference> newPrefs = new ArrayList<>();
+        for (final Preference existingPref : preferences) {
+            if (!existingPref.user.equals(user)) {
+                newPrefs.add(existingPref);
+            }
+        }
+        return new ProfileNetworkPreferenceList(newPrefs);
+    }
+
     public boolean isEmpty() {
         return preferences.isEmpty();
     }
diff --git a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
index 534dbe7..e682026 100644
--- a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
+++ b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
@@ -30,6 +30,8 @@
 import android.telephony.data.NrQosSessionAttributes;
 import android.util.Log;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.Objects;
 
 /**
@@ -149,6 +151,7 @@
 
     void sendEventEpsQosSessionAvailable(final QosSession session,
             final EpsBearerQosSessionAttributes attributes) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventEpsQosSessionAvailable: sending...");
             mCallback.onQosEpsBearerSessionAvailable(session, attributes);
@@ -159,6 +162,7 @@
 
     void sendEventNrQosSessionAvailable(final QosSession session,
             final NrQosSessionAttributes attributes) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventNrQosSessionAvailable: sending...");
             mCallback.onNrQosSessionAvailable(session, attributes);
@@ -168,6 +172,7 @@
     }
 
     void sendEventQosSessionLost(@NonNull final QosSession session) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventQosSessionLost: sending...");
             mCallback.onQosSessionLost(session);
@@ -185,6 +190,21 @@
         }
     }
 
+    private boolean validateOrSendErrorAndUnregister() {
+        final int exceptionType = mFilter.validate();
+        if (exceptionType != EX_TYPE_FILTER_NONE) {
+             log("validation fail before sending QosCallback.");
+             // Error callback is returned from Android T to prevent any disruption of application
+             // running on Android S.
+             if (SdkLevel.isAtLeastT()) {
+                sendEventQosCallbackError(exceptionType);
+                mQosCallbackTracker.unregisterCallback(mCallback);
+            }
+            return false;
+        }
+        return true;
+    }
+
     private static void log(@NonNull final String msg) {
         Log.d(TAG, msg);
     }
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/net/DelayedDiskWrite.java b/service/src/com/android/server/net/DelayedDiskWrite.java
new file mode 100644
index 0000000..41cb419
--- /dev/null
+++ b/service/src/com/android/server/net/DelayedDiskWrite.java
@@ -0,0 +1,147 @@
+/*
+ * 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 com.android.internal.annotations.VisibleForTesting;
+
+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 final Dependencies mDeps;
+
+    private HandlerThread mDiskWriteHandlerThread;
+    private Handler mDiskWriteHandler;
+    /* Tracks multiple writes on the same thread */
+    private int mWriteSequence = 0;
+
+    public DelayedDiskWrite() {
+        this(new Dependencies());
+    }
+
+    @VisibleForTesting
+    DelayedDiskWrite(Dependencies deps) {
+        mDeps = deps;
+    }
+
+    /**
+     * Dependencies class of DelayedDiskWrite, used for injection in tests.
+     */
+    @VisibleForTesting
+    static class Dependencies {
+        /**
+         * Create a HandlerThread to use in DelayedDiskWrite.
+         */
+        public HandlerThread makeHandlerThread() {
+            return new HandlerThread("DelayedDiskWriteThread");
+        }
+
+        /**
+         * Quit the HandlerThread looper.
+         */
+        public void quitHandlerThread(HandlerThread handlerThread) {
+            handlerThread.getLooper().quit();
+        }
+    }
+
+    /**
+     * 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 = mDeps.makeHandlerThread();
+                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) {
+                    mDeps.quitHandlerThread(mDiskWriteHandlerThread);
+                    mDiskWriteHandlerThread = null;
+                    mDiskWriteHandler = null;
+                }
+            }
+        }
+    }
+
+    private void loge(String s) {
+        Log.e(TAG, s);
+    }
+}
+
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index b23074d..58731e0 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
@@ -124,6 +140,30 @@
     ],
 }
 
+// defaults for tests that need to build against framework-connectivity's @hide APIs, but also
+// using fully @hide classes that are jarjared (because they have no API member). Similar to
+// framework-connectivity-test-defaults above but uses pre-jarjar class names.
+// Only usable from targets that have visibility on framework-connectivity-pre-jarjar, and apply
+// connectivity jarjar rules so that references to jarjared classes still match: this is limited to
+// connectivity internal tests only.
+java_defaults {
+    name: "framework-connectivity-internal-test-defaults",
+    sdk_version: "core_platform", // tests can use @CorePlatformApi's
+    libs: [
+        // order matters: classes in framework-connectivity are resolved before framework,
+        // meaning @hide APIs in framework-connectivity are resolved before @SystemApi
+        // stubs in framework
+        "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
+        "framework-tethering.impl",
+        "framework",
+
+        // if sdk_version="" this gets automatically included, but here we need to add manually.
+        "framework-res",
+    ],
+    defaults_visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+}
+
 // Defaults for tests that want to run in mainline-presubmit.
 // Not widely used because many of our tests have AndroidTest.xml files and
 // use the mainline-param config-descriptor metadata in AndroidTest.xml.
diff --git a/tests/common/AndroidTest_Coverage.xml b/tests/common/AndroidTest_Coverage.xml
index 7c8e710..48d26b8 100644
--- a/tests/common/AndroidTest_Coverage.xml
+++ b/tests/common/AndroidTest_Coverage.xml
@@ -14,10 +14,13 @@
 -->
 <configuration description="Runs coverage tests for Connectivity">
     <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
-        <option name="test-file-name" value="ConnectivityCoverageTests.apk" />
+      <option name="test-file-name" value="ConnectivityCoverageTests.apk" />
+      <option name="install-arg" value="-t" />
     </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/EthernetNetworkManagementExceptionTest.java b/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java
new file mode 100644
index 0000000..84b6e54
--- /dev/null
+++ b/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+public class EthernetNetworkManagementExceptionTest {
+    private static final String ERROR_MESSAGE = "Test error message";
+
+    @Test
+    public void testEthernetNetworkManagementExceptionParcelable() {
+        final EthernetNetworkManagementException e =
+                new EthernetNetworkManagementException(ERROR_MESSAGE);
+
+        assertParcelingIsLossless(e);
+    }
+
+    @Test
+    public void testEthernetNetworkManagementExceptionHasExpectedErrorMessage() {
+        final EthernetNetworkManagementException e =
+                new EthernetNetworkManagementException(ERROR_MESSAGE);
+
+        assertEquals(ERROR_MESSAGE, e.getMessage());
+    }
+}
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 4d85a57..581ee22 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;
@@ -44,6 +46,10 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk31;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -65,6 +71,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");
@@ -1253,7 +1262,20 @@
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+    public void testHasExcludeRoute() {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 24), RTN_UNICAST));
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 0), RTN_UNICAST));
+        assertFalse(lp.hasExcludeRoute());
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 32), RTN_THROW));
+        assertTrue(lp.hasExcludeRoute());
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("wlan0");
@@ -1268,4 +1290,37 @@
         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)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden on T or above")
+    @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/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 9ae5fab..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";
@@ -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/java/android/net/netstats/NetworkStatsCollectionTest.kt b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
new file mode 100644
index 0000000..368a519
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.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.netstats
+
+import android.net.NetworkIdentity
+import android.net.NetworkStatsCollection
+import android.net.NetworkStatsHistory
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+@ConnectivityModuleTest
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsCollectionTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    @Test
+    fun testBuilder() {
+        val ident = setOf<NetworkIdentity>()
+        val key1 = NetworkStatsCollection.Key(ident, /* uid */ 0, /* set */ 0, /* tag */ 0)
+        val key2 = NetworkStatsCollection.Key(ident, /* uid */ 1, /* set */ 0, /* tag */ 0)
+        val bucketDuration = 10L
+        val entry1 = NetworkStatsHistory.Entry(10, 10, 40, 4, 50, 5, 60)
+        val entry2 = NetworkStatsHistory.Entry(30, 10, 3, 41, 7, 1, 0)
+        val history1 = NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry1)
+                .addEntry(entry2)
+                .build()
+        val history2 = NetworkStatsHistory(10, 5)
+        val actualCollection = NetworkStatsCollection.Builder(bucketDuration)
+                .addEntry(key1, history1)
+                .addEntry(key2, history2)
+                .build()
+
+        // The builder will omit any entry with empty history. Thus, only history1
+        // is expected in the result collection.
+        val actualEntries = actualCollection.entries
+        assertEquals(1, actualEntries.size)
+        val actualHistory = actualEntries[key1] ?: fail("There should be an entry for $key1")
+        assertEquals(history1.entries, actualHistory.entries)
+    }
+}
diff --git a/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
new file mode 100644
index 0000000..f8e041a
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.NetworkStatsHistory
+import android.text.format.DateUtils
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+@ConnectivityModuleTest
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsHistoryTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    @Test
+    fun testBuilder() {
+        val entry1 = NetworkStatsHistory.Entry(10, 30, 40, 4, 50, 5, 60)
+        val entry2 = NetworkStatsHistory.Entry(30, 15, 3, 41, 7, 1, 0)
+        val entry3 = NetworkStatsHistory.Entry(7, 301, 11, 14, 31, 2, 80)
+        val statsEmpty = NetworkStatsHistory
+                .Builder(DateUtils.HOUR_IN_MILLIS, /* initialCapacity */ 10).build()
+        assertEquals(0, statsEmpty.entries.size)
+        assertEquals(DateUtils.HOUR_IN_MILLIS, statsEmpty.bucketDuration)
+        val statsSingle = NetworkStatsHistory
+                .Builder(DateUtils.HOUR_IN_MILLIS, /* initialCapacity */ 8)
+                .addEntry(entry1)
+                .build()
+        statsSingle.assertEntriesEqual(entry1)
+        assertEquals(DateUtils.HOUR_IN_MILLIS, statsSingle.bucketDuration)
+
+        // Verify the builder throws if the timestamp of added entry is not greater than
+        // that of any previously-added entry.
+        assertFailsWith(IllegalArgumentException::class) {
+            NetworkStatsHistory
+                    .Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
+                    .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+                    .build()
+        }
+
+        val statsMultiple = NetworkStatsHistory
+                .Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
+                .addEntry(entry3).addEntry(entry1).addEntry(entry2)
+                .build()
+        assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
+        statsMultiple.assertEntriesEqual(entry3, entry1, entry2)
+    }
+
+    fun NetworkStatsHistory.assertEntriesEqual(vararg entries: NetworkStatsHistory.Entry) {
+        assertEquals(entries.size, this.entries.size)
+        entries.forEachIndexed { i, element ->
+            assertEquals(element, this.entries[i])
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
new file mode 100644
index 0000000..192694b
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.NetworkStats.DEFAULT_NETWORK_ALL
+import android.net.NetworkStats.METERED_ALL
+import android.net.NetworkStats.METERED_YES
+import android.net.NetworkStats.ROAMING_YES
+import android.net.NetworkStats.ROAMING_ALL
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_BLUETOOTH
+import android.net.NetworkTemplate.MATCH_CARRIER
+import android.net.NetworkTemplate.MATCH_ETHERNET
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
+import android.net.NetworkTemplate.MATCH_PROXY
+import android.net.NetworkTemplate.MATCH_WIFI
+import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
+import android.net.NetworkTemplate.NETWORK_TYPE_ALL
+import android.net.NetworkTemplate.OEM_MANAGED_ALL
+import android.telephony.TelephonyManager
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+private const val TEST_IMSI1 = "imsi"
+private const val TEST_WIFI_KEY1 = "wifiKey1"
+private const val TEST_WIFI_KEY2 = "wifiKey2"
+
+@RunWith(JUnit4::class)
+@ConnectivityModuleTest
+class NetworkTemplateTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    @Test
+    fun testBuilderMatchRules() {
+        // Verify unknown match rules cannot construct templates.
+        listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(it).build()
+            }
+        }
+
+        // Verify hidden match rules cannot construct templates.
+        listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(it).build()
+            }
+        }
+
+        // Verify template which matches metered cellular and carrier networks with
+        // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
+        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+                    .setMeteredness(METERED_YES).build().let {
+                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                        assertEquals(expectedTemplate, it)
+                    }
+        }
+
+        // Verify template which matches roaming cellular and carrier networks with
+        // the given IMSI.
+        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+                    .setRoaming(ROAMING_YES).setMeteredness(METERED_YES).build().let {
+                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                                ROAMING_YES, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                        assertEquals(expectedTemplate, it)
+                    }
+        }
+
+        // Verify carrier template cannot be created without IMSI.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkTemplate.Builder(MATCH_CARRIER).build()
+        }
+
+        // Verify template which matches metered cellular networks,
+        // regardless of IMSI. See buildTemplateMobileWildcard.
+        NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf<String>(),
+                    METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches metered cellular networks and ratType.
+        // See NetworkTemplate#buildTemplateMobileWithRatType.
+        NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
+                .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
+                .build().let {
+                    val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
+                            arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                            ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
+                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                    assertEquals(expectedTemplate, it)
+                }
+
+        // Verify template which matches all wifi networks,
+        // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
+        NetworkTemplate.Builder(MATCH_WIFI).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf<String>(),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches wifi networks with the given Wifi Network Key.
+        // See buildTemplateWifi(wifiNetworkKey).
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches all wifi networks with the
+        // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
+        NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
+                .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+                    val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
+                            arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
+                            METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                    assertEquals(expectedTemplate, it)
+                }
+
+        // Verify template which matches ethernet and bluetooth networks.
+        // See buildTemplateEthernet and buildTemplateBluetooth.
+        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).build().let {
+                val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
+                        null /*subscriberIds*/, arrayOf<String>(),
+                        METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                        OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+                assertEquals(expectedTemplate, it)
+            }
+        }
+    }
+
+    @Test
+    fun testBuilderWifiNetworkKeys() {
+        // Verify template builder which generates same template with the given different
+        // sequence keys.
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+                setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
+            val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+                    setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches non-wifi networks with the given key is invalid.
+        listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
+                Integer.MAX_VALUE).forEach { matchRule ->
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
+            }
+        }
+
+        // Verify template which matches wifi networks with the given null key is invalid.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
+        }
+
+        // Verify template which matches wifi wildcard with the given empty key set.
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+                    arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+    }
+}
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/Android.bp b/tests/cts/hostside/Android.bp
index b684068..ac84e57 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -26,6 +26,7 @@
         "tradefed",
     ],
     static_libs: [
+        "CompatChangeGatingTestBase",
         "modules-utils-build-testing",
     ],
     // Tag this module as a cts test artifact
@@ -34,4 +35,12 @@
         "general-tests",
         "sts"
     ],
+    data: [
+        ":CtsHostsideNetworkTestsApp",
+        ":CtsHostsideNetworkTestsApp2",
+        ":CtsHostsideNetworkTestsApp3",
+        ":CtsHostsideNetworkTestsApp3PreT",
+        ":CtsHostsideNetworkTestsAppNext",
+    ],
+    per_testcase_directory: true,
 }
diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING
index fcec483..ab6de82 100644
--- a/tests/cts/hostside/TEST_MAPPING
+++ b/tests/cts/hostside/TEST_MAPPING
@@ -4,9 +4,6 @@
       "name": "CtsHostsideNetworkTests",
       "options": [
         {
-          "include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests"
-        },
-        {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         },
         {
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index 28437c2..e7b2815 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -28,5 +28,5 @@
     void sendNotification(int notificationId, String notificationType);
     void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
     void unregisterNetworkCallback();
-    void scheduleJob(in JobInfo jobInfo);
+    int scheduleJob(in JobInfo jobInfo);
 }
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 a840242..93e9dcd 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
@@ -16,6 +16,7 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.job.JobScheduler.RESULT_SUCCESS;
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.os.BatteryManager.BATTERY_PLUGGED_AC;
 import static android.os.BatteryManager.BATTERY_PLUGGED_USB;
@@ -149,9 +150,9 @@
 
     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 = 15_000;
+    private static final long BROADCAST_TIMEOUT_MS = 5_000;
 
     protected Context mContext;
     protected Instrumentation mInstrumentation;
@@ -216,7 +217,10 @@
             Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
                     + attempts + " attempts; sleeping "
                     + SLEEP_TIME_SEC + " seconds before trying again");
-            SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+            // No sleep after the last turn
+            if (attempts <= maxAttempts) {
+                SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+            }
         } while (attempts <= maxAttempts);
         assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
                 + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
@@ -327,7 +331,10 @@
             }
             Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
                     + "; sleeping 1s before trying again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on background state after "
                 + maxTries + " attempts: " + state);
@@ -346,7 +353,10 @@
             Log.d(TAG, "App not on foreground state on attempt #" + i
                     + "; sleeping 1s before trying again");
             turnScreenOn();
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on foreground state after "
                 + maxTries + " attempts: " + state);
@@ -364,7 +374,10 @@
             }
             Log.d(TAG, "App not on foreground service state on attempt #" + i
                     + "; sleeping 1s before trying again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on foreground service state after "
                 + maxTries + " attempts: " + state);
@@ -505,7 +518,10 @@
             Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
                     + checker.getExpected() + "' on attempt #" + i
                     + "; sleeping " + napTimeSeconds + "s before trying again");
-            SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+            }
         }
         fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
                 + maxTries
@@ -577,7 +593,10 @@
             }
             Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
                     + expected + ", got " + actual + "); sleeping 1s before polling again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
                 + ". Full list: " + uids);
@@ -737,7 +756,8 @@
 
     protected void assertAppIdle(boolean enabled) throws Exception {
         try {
-            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG, 15, 2, "Idle=" + enabled);
+            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
+                    30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + enabled);
         } catch (Throwable e) {
             throw e;
         }
@@ -764,7 +784,10 @@
                 return;
             }
             Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("app2 receiver is not ready in " + mUid);
     }
@@ -813,8 +836,6 @@
             return;
         } else if (type == TYPE_COMPONENT_ACTIVTIY) {
             turnScreenOn();
-            // Wait for screen-on state to propagate through the system.
-            SystemClock.sleep(2000);
             final CountDownLatch latch = new CountDownLatch(1);
             final Intent launchIntent = getIntentForComponent(type);
             final Bundle extras = new Bundle();
@@ -855,7 +876,8 @@
                     .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                     .setTransientExtras(extras)
                     .build();
-            mServiceClient.scheduleJob(jobInfo);
+            assertEquals("Error scheduling " + jobInfo,
+                    RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
             forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
             if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                 final int resultCode = result.get(0).first;
@@ -896,7 +918,7 @@
         final Intent intent = new Intent();
         if (type == TYPE_COMPONENT_ACTIVTIY) {
             intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
         } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
             intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
                     .setFlags(1);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
new file mode 100644
index 0000000..098f295
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.cts.net.hostside;
+
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class ConnOnActivityStartTest extends AbstractRestrictBackgroundNetworkTestCase {
+    private static final int TEST_ITERATION_COUNT = 5;
+
+    @Before
+    public final void setUp() throws Exception {
+        super.setUp();
+        resetDeviceState();
+    }
+
+    @After
+    public final void tearDown() throws Exception {
+        super.tearDown();
+        resetDeviceState();
+    }
+
+    private void resetDeviceState() throws Exception {
+        resetBatteryState();
+        setBatterySaverMode(false);
+        setRestrictBackground(false);
+        setAppIdle(false);
+        setDozeMode(false);
+    }
+
+
+    @Test
+    @RequiredProperties({BATTERY_SAVER_MODE})
+    public void testStartActivity_batterySaver() throws Exception {
+        setBatterySaverMode(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver");
+    }
+
+    @Test
+    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
+    public void testStartActivity_dataSaver() throws Exception {
+        setRestrictBackground(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver");
+    }
+
+    @Test
+    @RequiredProperties({DOZE_MODE})
+    public void testStartActivity_doze() throws Exception {
+        setDozeMode(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_doze");
+    }
+
+    @Test
+    @RequiredProperties({APP_STANDBY_MODE})
+    public void testStartActivity_appStandby() throws Exception {
+        turnBatteryOn();
+        setAppIdle(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby");
+    }
+
+    private void assertLaunchedActivityHasNetworkAccess(String testName) throws Exception {
+        for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+            Log.i(TAG, testName + " start #" + i);
+            launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+            getUiDevice().pressHome();
+            assertBackgroundState();
+            Log.i(TAG, testName + " end #" + i);
+        }
+    }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 8b70f9b..0610774 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -107,7 +107,7 @@
         mService.unregisterNetworkCallback();
     }
 
-    public void scheduleJob(JobInfo jobInfo) throws RemoteException {
-        mService.scheduleJob(jobInfo);
+    public int scheduleJob(JobInfo jobInfo) throws RemoteException {
+        return mService.scheduleJob(jobInfo);
     }
 }
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..c53276b 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;
@@ -56,6 +57,7 @@
 import android.util.Log;
 
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
 
 import com.android.compatibility.common.util.AppStandbyUtils;
 import com.android.compatibility.common.util.BatteryUtils;
@@ -99,6 +101,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 +113,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 +391,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;
     }
@@ -430,6 +439,10 @@
         return InstrumentationRegistry.getInstrumentation();
     }
 
+    public static UiDevice getUiDevice() {
+        return UiDevice.getInstance(getInstrumentation());
+    }
+
     // When power saver mode or restrict background enabled or adding any white/black list into
     // those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
     // this function and using PollingCheck to try to make sure the uid has updated and reduce the
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 01c8cd2..edfaf9f 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -23,6 +23,7 @@
     defaults: ["cts_support_defaults"],
     sdk_version: "test_current",
     static_libs: [
+        "androidx.annotation_annotation",
         "CtsHostsideNetworkTestsAidl",
         "NetworkStackApiStableShims",
     ],
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
index 9fdb9c9..82f13ae 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -29,6 +29,9 @@
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.WindowManager;
+
+import androidx.annotation.GuardedBy;
 
 import com.android.cts.net.hostside.INetworkStateObserver;
 
@@ -37,27 +40,24 @@
  */
 public class MyActivity extends Activity {
 
+    @GuardedBy("this")
     private BroadcastReceiver finishCommandReceiver = null;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Log.d(TAG, "MyActivity.onCreate()");
-        Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
-        finishCommandReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                Log.d(TAG, "Finishing MyActivity");
-                MyActivity.this.finish();
-            }
-        };
-        registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY));
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
     }
 
     @Override
     public void finish() {
-        if (finishCommandReceiver != null) {
-            unregisterReceiver(finishCommandReceiver);
+        synchronized (this) {
+            if (finishCommandReceiver != null) {
+                unregisterReceiver(finishCommandReceiver);
+                finishCommandReceiver = null;
+            }
         }
         super.finish();
     }
@@ -69,6 +69,31 @@
     }
 
     @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        Log.d(TAG, "MyActivity.onNewIntent()");
+        setIntent(intent);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        Log.d(TAG, "MyActivity.onResume(): " + getIntent());
+        Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
+        synchronized (this) {
+            finishCommandReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    Log.d(TAG, "Finishing MyActivity");
+                    MyActivity.this.finish();
+                }
+            };
+            registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
+                    Context.RECEIVER_EXPORTED);
+        }
+    }
+
+    @Override
     protected void onDestroy() {
         Log.d(TAG, "MyActivity.onDestroy()");
         super.onDestroy();
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index f2a7b3f..3ed5391 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -165,10 +165,10 @@
         }
 
         @Override
-        public void scheduleJob(JobInfo jobInfo) {
+        public int scheduleJob(JobInfo jobInfo) {
             final JobScheduler jobScheduler = getApplicationContext()
                     .getSystemService(JobScheduler.class);
-            jobScheduler.schedule(jobInfo);
+            return jobScheduler.schedule(jobInfo);
         }
       };
 
diff --git a/tests/cts/hostside/app3/Android.bp b/tests/cts/hostside/app3/Android.bp
new file mode 100644
index 0000000..141cf03
--- /dev/null
+++ b/tests/cts/hostside/app3/Android.bp
@@ -0,0 +1,54 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "CtsHostsideNetworkTestsApp3Defaults",
+    srcs: ["src/**/*.java"],
+    libs: [
+        "junit",
+    ],
+    static_libs: [
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+    ],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp3",
+    defaults: [
+        "cts_support_defaults",
+        "CtsHostsideNetworkTestsApp3Defaults",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp3PreT",
+    target_sdk_version: "31",
+    defaults: [
+        "cts_support_defaults",
+        "CtsHostsideNetworkTestsApp3Defaults",
+    ],
+}
diff --git a/tests/cts/hostside/app3/AndroidManifest.xml b/tests/cts/hostside/app3/AndroidManifest.xml
new file mode 100644
index 0000000..eabcacb
--- /dev/null
+++ b/tests/cts/hostside/app3/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.net.hostside.app3">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.net.hostside.app3" />
+
+</manifest>
diff --git a/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
new file mode 100644
index 0000000..a1a8209
--- /dev/null
+++ b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.cts.net.hostside.app3;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests to verify {@link LinkProperties#getRoutes} behavior, depending on
+ * {@LinkProperties#EXCLUDED_ROUTES} change state.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExcludedRoutesGatingTest {
+    @Before
+    public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+    }
+
+    @After
+    public void tearDown() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testExcludedRoutesChangeEnabled() {
+        final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+        // Excluded routes change is enabled: non-RTN_UNICAST routes are visible.
+        assertEquals(2, lp.getRoutes().size());
+        assertEquals(2, lp.getAllRoutes().size());
+    }
+
+    @Test
+    public void testExcludedRoutesChangeDisabled() {
+        final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+        // Excluded routes change is disabled: non-RTN_UNICAST routes are filtered out.
+        assertEquals(0, lp.getRoutes().size());
+        assertEquals(0, lp.getAllRoutes().size());
+    }
+
+    private LinkProperties makeLinkPropertiesWithExcludedRoutes() {
+        final LinkProperties lp = new LinkProperties();
+
+        lp.addRoute(new RouteInfo(new IpPrefix("10.0.0.0/8"), null, null, RouteInfo.RTN_THROW));
+        lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"), null, null,
+                RouteInfo.RTN_UNREACHABLE));
+
+        return lp;
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
new file mode 100644
index 0000000..3387fd7
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.cts.net;
+
+public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
+    private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        uninstallPackage(TEST_APP2_PKG, false);
+        installPackage(TEST_APP2_APK);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+
+        uninstallPackage(TEST_APP2_PKG, true);
+    }
+
+    public void testStartActivity_batterySaver() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
+    }
+
+    public void testStartActivity_dataSaver() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
+    }
+
+    public void testStartActivity_doze() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
+    }
+
+    public void testStartActivity_appStandby() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
new file mode 100644
index 0000000..b65fb6b
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
@@ -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.
+ */
+
+package com.android.cts.net;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+
+import java.util.Set;
+
+/**
+ * Tests for the {@link android.net.LinkProperties#EXCLUDED_ROUTES} compatibility change.
+ */
+public class HostsideLinkPropertiesGatingTests extends CompatChangeGatingTestCase {
+    private static final String TEST_APK = "CtsHostsideNetworkTestsApp3.apk";
+    private static final String TEST_APK_PRE_T = "CtsHostsideNetworkTestsApp3PreT.apk";
+    private static final String TEST_PKG = "com.android.cts.net.hostside.app3";
+    private static final String TEST_CLASS = ".ExcludedRoutesGatingTest";
+
+    private static final long EXCLUDED_ROUTES_CHANGE_ID = 186082280;
+
+    protected void tearDown() throws Exception {
+        uninstallPackage(TEST_PKG, true);
+    }
+
+    public void testExcludedRoutesChangeEnabled() throws Exception {
+        installPackage(TEST_APK, true);
+        runDeviceCompatTest("testExcludedRoutesChangeEnabled");
+    }
+
+    public void testExcludedRoutesChangeDisabledPreT() throws Exception {
+        installPackage(TEST_APK_PRE_T, true);
+        runDeviceCompatTest("testExcludedRoutesChangeDisabled");
+    }
+
+    public void testExcludedRoutesChangeDisabledByOverride() throws Exception {
+        installPackage(TEST_APK, true);
+        runDeviceCompatTestWithChangeDisabled("testExcludedRoutesChangeDisabled");
+    }
+
+    public void testExcludedRoutesChangeEnabledByOverridePreT() throws Exception {
+        installPackage(TEST_APK_PRE_T, true);
+        runDeviceCompatTestWithChangeEnabled("testExcludedRoutesChangeEnabled");
+    }
+
+    private void runDeviceCompatTest(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(), Set.of());
+    }
+
+    private void runDeviceCompatTestWithChangeEnabled(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(EXCLUDED_ROUTES_CHANGE_ID),
+                Set.of());
+    }
+
+    private void runDeviceCompatTestWithChangeDisabled(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(),
+                Set.of(EXCLUDED_ROUTES_CHANGE_ID));
+    }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index e979a3b..a6ed762 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -62,6 +62,7 @@
     // sdk_version: "current",
     platform_apis: true,
     required: ["ConnectivityChecker"],
+    test_config_template: "AndroidTestTemplate.xml",
 }
 
 // Networking CTS tests for development and release. These tests always target the platform SDK
@@ -79,7 +80,16 @@
         "cts",
         "general-tests",
     ],
-    test_config_template: "AndroidTestTemplate.xml",
+}
+
+java_defaults {
+    name: "CtsNetTestCasesApiStableDefaults",
+    // TODO: CTS should not depend on the entirety of the networkstack code.
+    static_libs: [
+        "NetworkStackApiStableLib",
+    ],
+    jni_uses_sdk_apis: true,
+    min_sdk_version: "29",
 }
 
 // Networking CTS tests that target the latest released SDK. These tests can be installed on release
@@ -87,14 +97,11 @@
 // on release devices.
 android_test {
     name: "CtsNetTestCasesLatestSdk",
-    defaults: ["CtsNetTestCasesDefaults"],
-    // TODO: CTS should not depend on the entirety of the networkstack code.
-    static_libs: [
-        "NetworkStackApiStableLib",
+    defaults: [
+        "CtsNetTestCasesDefaults",
+        "CtsNetTestCasesApiStableDefaults",
     ],
-    jni_uses_sdk_apis: true,
-    min_sdk_version: "29",
-    target_sdk_version: "30",
+    target_sdk_version: "33",
     test_suites: [
         "general-tests",
         "mts-dnsresolver",
@@ -102,5 +109,21 @@
         "mts-tethering",
         "mts-wifi",
     ],
-    test_config_template: "AndroidTestTemplate.xml",
 }
+
+android_test {
+    name: "CtsNetTestCasesMaxTargetSdk31",  // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+    defaults: [
+        "CtsNetTestCasesDefaults",
+        "CtsNetTestCasesApiStableDefaults",
+    ],
+    target_sdk_version: "31",
+    package_name: "android.net.cts.maxtargetsdk31",  // CTS package names must be unique.
+    instrumentation_target_package: "android.net.cts.maxtargetsdk31",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts-networking",
+    ],
+}
+
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 3b47100..6b5bb93 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -44,7 +44,8 @@
              android.permission.MANAGE_TEST_NETWORKS
     -->
 
-    <application android:usesCleartextTraffic="true">
+    <application android:debuggable="true"
+                 android:usesCleartextTraffic="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
     </application>
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 48a1c79..d2fb04a 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -33,9 +33,36 @@
     <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="package" value="{PACKAGE}" />
         <option name="runtime-hint" value="9m4s" />
         <option name="hidden-api-checks" value="false" />
         <option name="isolated-storage" value="false" />
+        <!-- Test filter that allows test APKs to select which tests they want to run by annotating
+             those tests with an annotation matching the name of the APK.
+
+             This allows us to maintain one AndroidTestTemplate.xml for all CtsNetTestCases*.apk,
+             and have CtsNetTestCases and CtsNetTestCasesLatestSdk run all tests, but have
+             CtsNetTestCasesMaxTargetSdk31 run only tests that require target SDK 31.
+
+             This relies on the fact that if the class specified in include-annotation exists, then
+             the runner will only run the tests annotated with that annotation, but if it does not,
+             the runner will run all the tests. -->
+        <option name="include-annotation" value="com.android.testutils.filters.{MODULE}" />
     </test>
+    <!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+        one of the Mainline modules below is present on the device used for testing. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <!-- Tethering Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+        <!-- Tethering Module (AOSP version). -->
+        <option name="mainline-module-package-name" value="com.android.tethering" />
+        <!-- NetworkStack Module (internal version). Should always be installed with CaptivePortalLogin. -->
+        <option name="mainline-module-package-name" value="com.google.android.networkstack" />
+        <!-- NetworkStack Module (AOSP version). Should always be installed with CaptivePortalLogin. -->
+        <option name="mainline-module-package-name" value="com.android.networkstack" />
+        <!-- Resolver Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.resolv" />
+        <!-- Resolver Module (AOSP version). -->
+        <option name="mainline-module-package-name" value="com.android.resolv" />
+    </object>
 </configuration>
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/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 5b37294..9b81a56 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -51,5 +51,8 @@
         "cts",
         "general-tests",
     ],
-
+    data: [
+        ":CtsNetTestAppForApi23",
+    ],
+    per_testcase_directory: true,
 }
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 d40bc9f..d97ebcb 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -37,6 +37,10 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.ConnectivityManager.EXTRA_NETWORK;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_REQUEST;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
@@ -52,6 +56,7 @@
 import static android.net.ConnectivityManager.TYPE_PROXY;
 import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -151,6 +156,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;
@@ -163,7 +169,6 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.Pair;
 import android.util.Range;
 
 import androidx.test.InstrumentationRegistry;
@@ -181,6 +186,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRuleKt;
+import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
 import com.android.testutils.TestHttpServer;
@@ -202,6 +208,8 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
 import java.net.HttpURLConnection;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -216,6 +224,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
@@ -253,6 +262,7 @@
     private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
     private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 5_000;
     private static final int NO_CALLBACK_TIMEOUT_MS = 100;
+    private static final int SOCKET_TIMEOUT_MS = 100;
     private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
     private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
     // device could have only one interface: data, wifi.
@@ -590,6 +600,7 @@
     }
 
     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @AppModeFull(reason = "Cannot get installed packages in instant app mode")
     @Test
     public void testGetRedactedLinkPropertiesForPackage() throws Exception {
         final String groundedPkg = findPackageByPermissions(
@@ -660,10 +671,12 @@
 
             // CaptivePortalApiUrl & CaptivePortalData will be preserved if the given uid holds the
             // NETWORK_SETTINGS permission.
-            assertEquals(capportUrl,
+            assertNotNull(lp.getCaptivePortalApiUrl());
+            assertNotNull(lp.getCaptivePortalData());
+            assertEquals(lp.getCaptivePortalApiUrl(),
                     mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                             .getCaptivePortalApiUrl());
-            assertEquals(capportData,
+            assertEquals(lp.getCaptivePortalData(),
                     mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                             .getCaptivePortalData());
         });
@@ -675,6 +688,7 @@
     }
 
     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @AppModeFull(reason = "Cannot get installed packages in instant app mode")
     @Test
     public void testGetRedactedNetworkCapabilitiesForPackage() throws Exception {
         final String groundedPkg = findPackageByPermissions(
@@ -885,9 +899,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) {
@@ -1588,51 +1614,7 @@
 
     private static boolean isTcpKeepaliveSupportedByKernel() {
         final String kVersionString = VintfRuntimeInfo.getKernelVersion();
-        return compareMajorMinorVersion(kVersionString, "4.8") >= 0;
-    }
-
-    private static Pair<Integer, Integer> getVersionFromString(String version) {
-        // Only gets major and minor number of the version string.
-        final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
-        final Matcher m = versionPattern.matcher(version);
-        if (m.matches()) {
-            final int major = Integer.parseInt(m.group(1));
-            final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3));
-            return new Pair<>(major, minor);
-        } else {
-            return new Pair<>(0, 0);
-        }
-    }
-
-    // TODO: Move to util class.
-    private static int compareMajorMinorVersion(final String s1, final String s2) {
-        final Pair<Integer, Integer> v1 = getVersionFromString(s1);
-        final Pair<Integer, Integer> v2 = getVersionFromString(s2);
-
-        if (v1.first == v2.first) {
-            return Integer.compare(v1.second, v2.second);
-        } else {
-            return Integer.compare(v1.first, v2.first);
-        }
-    }
-
-    /**
-     * Verifies that version string compare logic returns expected result for various cases.
-     * Note that only major and minor number are compared.
-     */
-    @Test
-    public void testMajorMinorVersionCompare() {
-        assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8"));
-        assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1"));
-        assertEquals(1, compareMajorMinorVersion("5.0", "4.8"));
-        assertEquals(1, compareMajorMinorVersion("5", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("5", "5.0"));
-        assertEquals(1, compareMajorMinorVersion("5-beta1", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8.0.0", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8-RC1", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8", "4.8"));
-        assertEquals(-1, compareMajorMinorVersion("3.10", "4.8.0"));
-        assertEquals(-1, compareMajorMinorVersion("4.7.10.10", "4.8"));
+        return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.8") >= 0;
     }
 
     /**
@@ -3195,7 +3177,7 @@
     @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
     @Test
     public void testUidsAllowedOnRestrictedNetworks() throws Exception {
-        assumeTrue(TestUtils.shouldTestSApis());
+        assumeTestSApis();
 
         // TODO (b/175199465): figure out a reasonable permission check for
         //  setUidsAllowedOnRestrictedNetworks that allows tests but not system-external callers.
@@ -3208,10 +3190,10 @@
         // because it has been just installed to device. In case the uid is existed in setting
         // mistakenly, try to remove the uid and set correct uids to setting.
         originalUidsAllowedOnRestrictedNetworks.remove(uid);
-        runWithShellPermissionIdentity(() ->
-                ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                        mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+        runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
 
+        // File a restricted network request with permission first to hold the connection.
         final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
@@ -3223,6 +3205,19 @@
         runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS);
 
+        // File another restricted network request without permission.
+        final TestableNetworkCallback restrictedNetworkCb = new TestableNetworkCallback();
+        final NetworkRequest restrictedRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
+                        TEST_RESTRICTED_NW_IFACE_NAME))
+                .build();
+        // Uid is not in allowed list and no permissions. Expect that SecurityException will throw.
+        assertThrows(SecurityException.class,
+                () -> mCm.requestNetwork(restrictedRequest, restrictedNetworkCb));
+
         final NetworkAgent agent = createRestrictedNetworkAgent(mContext);
         final Network network = agent.getNetwork();
 
@@ -3242,19 +3237,28 @@
             final Set<Integer> newUidsAllowedOnRestrictedNetworks =
                     new ArraySet<>(originalUidsAllowedOnRestrictedNetworks);
             newUidsAllowedOnRestrictedNetworks.add(uid);
-            runWithShellPermissionIdentity(() ->
-                    ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                            mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+            runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                    mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
             // Wait a while for sending allowed uids on the restricted network to netd.
-            // TODD: Have a significant signal to know the uids has been send to netd.
+            // TODD: Have a significant signal to know the uids has been sent to netd.
             assertBindSocketToNetworkSuccess(network);
+
+            if (TestUtils.shouldTestTApis()) {
+                // Uid is in allowed list. Try file network request again.
+                requestNetwork(restrictedRequest, restrictedNetworkCb);
+                // Verify that the network is restricted.
+                restrictedNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
+                        NETWORK_CALLBACK_TIMEOUT_MS,
+                        entry -> network.equals(entry.getNetwork())
+                                && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                                .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+            }
         } finally {
             agent.unregister();
 
             // Restore setting.
-            runWithShellPermissionIdentity(() ->
-                    ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                            mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+            runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                    mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
         }
     }
 
@@ -3278,6 +3282,117 @@
         assertTrue(dumpOutput, dumpOutput.contains("BPF map content"));
     }
 
+    private void checkFirewallBlocking(final DatagramSocket srcSock, final DatagramSocket dstSock,
+            final boolean expectBlock) throws Exception {
+        final Random random = new Random();
+        final byte[] sendData = new byte[100];
+        random.nextBytes(sendData);
+
+        final DatagramPacket pkt = new DatagramPacket(sendData, sendData.length,
+                InetAddresses.parseNumericAddress("::1"), dstSock.getLocalPort());
+        try {
+            srcSock.send(pkt);
+        } catch (IOException e) {
+            if (expectBlock) {
+                return;
+            }
+            fail("Expect not to be blocked by firewall but sending packet was blocked");
+        }
+
+        if (expectBlock) {
+            fail("Expect to be blocked by firewall but sending packet was not blocked");
+        }
+
+        dstSock.receive(pkt);
+        assertArrayEquals(sendData, pkt.getData());
+    }
+
+    private static final boolean EXPECT_PASS = false;
+    private static final boolean EXPECT_BLOCK = true;
+
+    private void doTestFirewallBlockingDenyRule(final int chain) {
+        runWithShellPermissionIdentity(() -> {
+            try (DatagramSocket srcSock = new DatagramSocket();
+                 DatagramSocket dstSock = new DatagramSocket()) {
+                dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+                // No global config, No uid config
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // Has global config, No uid config
+                mCm.setFirewallChainEnabled(chain, true /* enable */);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // Has global config, Has uid config
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);
+
+                // No global config, Has uid config
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // No global config, No uid config
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+            } finally {
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+            }
+        }, NETWORK_SETTINGS);
+    }
+
+    private void doTestFirewallBlockingAllowRule(final int chain) {
+        runWithShellPermissionIdentity(() -> {
+            try (DatagramSocket srcSock = new DatagramSocket();
+                 DatagramSocket dstSock = new DatagramSocket()) {
+                dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+                // No global config, No uid config
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // Has global config, No uid config
+                mCm.setFirewallChainEnabled(chain, true /* enable */);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);
+
+                // Has global config, Has uid config
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // No global config, Has uid config
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // No global config, No uid config
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+            } finally {
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+            }
+        }, NETWORK_SETTINGS);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testFirewallBlocking() {
+        // Following tests affect the actual state of networking on the device after the test.
+        // This might cause unexpected behaviour of the device. So, we skip them for now.
+        // We will enable following tests after adding the logic of firewall state restoring.
+        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_DOZABLE);
+        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_POWERSAVE);
+        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_RESTRICTED);
+        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+
+        // doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_STANDBY);
+        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_1);
+        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_2);
+    }
+
+    private void assumeTestSApis() {
+        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+        // shims, and @IgnoreUpTo does not check that.
+        assumeTrue(TestUtils.shouldTestSApis());
+    }
+
     private void unregisterRegisteredCallbacks() {
         for (NetworkCallback callback: mRegisteredCallbacks) {
             mCm.unregisterNetworkCallback(callback);
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index c6fc38f..0c53411 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -49,6 +49,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
 import android.system.ErrnoException;
 import android.util.Log;
 
@@ -727,6 +728,18 @@
 
     @Test
     public void testPrivateDnsBypass() throws InterruptedException {
+        final String dataStallSetting = Settings.Global.getString(mCR,
+                Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK);
+        Settings.Global.putInt(mCR, Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK, 0);
+        try {
+            doTestPrivateDnsBypass();
+        } finally {
+            Settings.Global.putString(mCR, Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK,
+                    dataStallSetting);
+        }
+    }
+
+    private void doTestPrivateDnsBypass() throws InterruptedException {
         final Network[] testNetworks = getTestableNetworks();
 
         // Set an invalid private DNS server
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index ea98289..bbac09b 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,30 +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()
 
@@ -114,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)
@@ -149,7 +142,9 @@
         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)
         }
 
@@ -163,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()
     }
 
@@ -191,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.
@@ -210,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)
@@ -232,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
             }
         }
@@ -280,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)
         }
     }
 
@@ -337,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)
 
@@ -379,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)
 
@@ -388,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)
     }
@@ -402,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 */
@@ -433,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)
@@ -452,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)
@@ -461,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)
         }
     }
 
@@ -490,44 +601,57 @@
         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)
         }
     }
 
     @Test
     fun testParcelingDscpPolicyIsLossless(): Unit = createConnectedNetworkAgent().let {
                 (agent, callback) ->
+        val policyId = 1
+        val dscpValue = 1
+        val range = Range(4444, 4444)
+        val srcPort = 555
+
         // Check that policy with partial parameters is lossless.
-        val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
-        assertParcelingIsLossless(policy);
+        val policy = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range).build()
+        assertEquals(policyId, policy.policyId)
+        assertEquals(dscpValue, policy.dscpValue)
+        assertEquals(range, policy.destinationPortRange)
+        assertParcelingIsLossless(policy)
 
         // Check that policy with all parameters is lossless.
-        val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444))
+        val policy2 = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range)
                 .setSourceAddress(LOCAL_IPV4_ADDRESS)
                 .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+                .setSourcePort(srcPort)
                 .setProtocol(IPPROTO_UDP).build()
-        assertParcelingIsLossless(policy2);
+        assertEquals(policyId, policy2.policyId)
+        assertEquals(dscpValue, policy2.dscpValue)
+        assertEquals(range, policy2.destinationPortRange)
+        assertEquals(TEST_TARGET_IPV4_ADDR, policy2.destinationAddress)
+        assertEquals(LOCAL_IPV4_ADDRESS, policy2.sourceAddress)
+        assertEquals(srcPort, policy2.sourcePort)
+        assertEquals(IPPROTO_UDP, policy2.protocol)
+        assertParcelingIsLossless(policy2)
     }
 }
 
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 0a32f09..db24b44 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -15,57 +15,129 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.ROLE_CLIENT
+import android.net.EthernetManager.ROLE_NONE
+import android.net.EthernetManager.ROLE_SERVER
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_DOWN
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.EthernetManager.TetheredInterfaceCallback
+import android.net.EthernetManager.TetheredInterfaceRequest
+import android.net.EthernetNetworkSpecifier
+import android.net.InetAddresses
 import android.net.IpConfiguration
+import android.net.MacAddress
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
 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 org.junit.runner.RunWith
-import kotlin.test.assertNull
-import kotlin.test.fail
 import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Build
 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 java.util.concurrent.Executor
+import android.os.SystemProperties
+import android.platform.test.annotations.AppModeFull
+import android.util.ArraySet
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.net.module.util.TrackRecord
+import com.android.testutils.anyNetwork
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReader
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet6Address
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
 import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
 
-private const val TIMEOUT_MS = 1000L
+// TODO: try to lower this timeout in the future. Currently, ethernet tests are still flaky because
+// the interface is not ready fast enough (mostly due to the up / up / down / up issue).
+private const val TIMEOUT_MS = 2000L
 private const val NO_CALLBACK_TIMEOUT_MS = 200L
 private val DEFAULT_IP_CONFIGURATION = IpConfiguration(IpConfiguration.IpAssignment.DHCP,
     IpConfiguration.ProxySettings.NONE, null, null)
+private val ETH_REQUEST: NetworkRequest = NetworkRequest.Builder()
+    .addTransportType(TRANSPORT_TEST)
+    .addTransportType(TRANSPORT_ETHERNET)
+    .removeCapability(NET_CAPABILITY_TRUSTED)
+    .build()
 
 @AppModeFull(reason = "Instant apps can't access EthernetManager")
-@RunWith(AndroidJUnit4::class)
+// EthernetManager is not updatable before T, so tests do not need to be backwards compatible.
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 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 em by lazy { context.getSystemService(EthernetManager::class.java) }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
 
-    private val createdIfaces = ArrayList<TestNetworkInterface>()
-    private val addedListeners = ArrayList<InterfaceStateListener>()
+    private val ifaceListener = EthernetStateListener()
+    private val createdIfaces = ArrayList<EthernetTestInterface>()
+    private val addedListeners = ArrayList<EthernetStateListener>()
+    private val networkRequests = ArrayList<TestableNetworkCallback>()
+
+    private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null
+
+    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 = 1500
+            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>
@@ -99,86 +171,364 @@
             return event as T
         }
 
-        fun expectCallback(iface: TestNetworkInterface, state: Int, role: Int) {
-            expectCallback(InterfaceStateChanged(iface.interfaceName, state, role,
-                if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null))
+        fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) {
+            expectCallback(createChangeEvent(iface.interfaceName, state, role))
         }
 
+        fun createChangeEvent(iface: String, state: Int, role: Int) =
+                InterfaceStateChanged(iface, 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 eventuallyExpect(expected: CallbackEntry) = events.poll(TIMEOUT_MS) { it == expected }
+
+        fun eventuallyExpect(interfaceName: String, state: Int, role: Int) {
+            assertNotNull(eventuallyExpect(createChangeEvent(interfaceName, state, role)))
+        }
+
+        fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
+            eventuallyExpect(iface.interfaceName, state, role)
+        }
+
         fun assertNoCallback() {
             val cb = events.poll(NO_CALLBACK_TIMEOUT_MS)
             assertNull(cb, "Expected no callback but got $cb")
         }
     }
 
-    @Test
-    public fun testCallbacks() {
-        val executor = HandlerExecutor(Handler(Looper.getMainLooper()))
+    private class TetheredInterfaceListener : TetheredInterfaceCallback {
+        private val available = CompletableFuture<String>()
 
-        // If an interface exists when the callback is registered, it is reported on registration.
-        val iface = runAsShell(MANAGE_TEST_NETWORKS) {
-            createInterface()
+        override fun onAvailable(iface: String) {
+            available.complete(iface)
         }
-        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 = runAsShell(MANAGE_TEST_NETWORKS) {
-            createInterface()
+        override fun onUnavailable() {
+            available.completeExceptionally(IllegalStateException("onUnavailable was called"))
         }
-        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)
+        fun expectOnAvailable(): String {
+            return available.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        }
 
-        removeInterface(iface2)
-        listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
-        listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
-        listener.assertNoCallback()
+        fun expectOnUnavailable() {
+            // Assert that the future fails with the IllegalStateException from the
+            // completeExceptionally() call inside onUnavailable.
+            assertFailsWith(IllegalStateException::class) {
+                try {
+                    available.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+                } catch (e: ExecutionException) {
+                    throw e.cause!!
+                }
+            }
+        }
     }
 
     @Before
     fun setUp() {
-        runAsShell(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS) {
-            em.setIncludeTestInterfaces(true)
-        }
+        setIncludeTestInterfaces(true)
+        addInterfaceStateListener(ifaceListener)
     }
 
     @After
     fun tearDown() {
-        runAsShell(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS) {
-            em.setIncludeTestInterfaces(false)
-            for (iface in createdIfaces) {
-                if (iface.fileDescriptor.fileDescriptor.valid()) iface.fileDescriptor.close()
-            }
-            for (listener in addedListeners) {
-                em.removeInterfaceStateListener(listener)
-            }
+        setIncludeTestInterfaces(false)
+        for (iface in createdIfaces) {
+            iface.destroy()
+            ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
         }
+        for (listener in addedListeners) {
+            em.removeInterfaceStateListener(listener)
+        }
+        networkRequests.forEach { cm.unregisterNetworkCallback(it) }
+        releaseTetheredInterface()
     }
 
-    private fun addInterfaceStateListener(executor: Executor, listener: InterfaceStateListener) {
-        em.addInterfaceStateListener(executor, listener)
+    private fun addInterfaceStateListener(listener: EthernetStateListener) {
+        runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
+            em.addInterfaceStateListener(HandlerExecutor(Handler(Looper.getMainLooper())), listener)
+        }
         addedListeners.add(listener)
     }
 
-    private fun createInterface(): TestNetworkInterface {
-        val tnm = context.getSystemService(TestNetworkManager::class.java)
-        return tnm.createTapInterface(false /* bringUp */).also { createdIfaces.add(it) }
+    private fun createInterface(): EthernetTestInterface {
+        val iface = EthernetTestInterface(
+            context,
+            Handler(Looper.getMainLooper())
+        ).also { createdIfaces.add(it) }
+        with(ifaceListener) {
+            // when an interface comes up, we should always see a down cb before an up cb.
+            eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+            expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+        }
+        return iface
     }
 
-    private fun removeInterface(iface: TestNetworkInterface) {
-        iface.fileDescriptor.close()
-        createdIfaces.remove(iface)
+    private fun setIncludeTestInterfaces(value: Boolean) {
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(value)
+        }
     }
-}
\ No newline at end of file
+
+    private fun removeInterface(iface: EthernetTestInterface) {
+        iface.destroy()
+        createdIfaces.remove(iface)
+        ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
+    }
+
+    private fun requestNetwork(request: NetworkRequest): TestableNetworkCallback {
+        return TestableNetworkCallback().also {
+            cm.requestNetwork(request, it)
+            networkRequests.add(it)
+        }
+    }
+
+    private fun releaseNetwork(cb: TestableNetworkCallback) {
+        cm.unregisterNetworkCallback(cb)
+        networkRequests.remove(cb)
+    }
+
+    private fun requestTetheredInterface() = TetheredInterfaceListener().also {
+        tetheredInterfaceRequest = runAsShell(NETWORK_SETTINGS) {
+            em.requestTetheredInterface(HandlerExecutor(Handler(Looper.getMainLooper())), it)
+        }
+    }
+
+    private fun releaseTetheredInterface() {
+        runAsShell(NETWORK_SETTINGS) {
+            tetheredInterfaceRequest?.release()
+            tetheredInterfaceRequest = null
+        }
+    }
+
+    private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) =
+        NetworkRequest.Builder(NetworkRequest(ETH_REQUEST))
+            .setNetworkSpecifier(EthernetNetworkSpecifier(ifaceName)).build()
+
+    // It can take multiple seconds for the network to become available.
+    private fun TestableNetworkCallback.expectAvailable() =
+        expectCallback<Available>(anyNetwork(), 5000/*ms timeout*/).network
+
+    // b/233534110: eventuallyExpect<Lost>() does not advance ReadHead, use
+    // eventuallyExpect(Lost::class) instead.
+    private fun TestableNetworkCallback.eventuallyExpectLost(n: Network? = null) =
+        eventuallyExpect(Lost::class, TIMEOUT_MS) { n?.equals(it.network) ?: true }
+
+    private fun TestableNetworkCallback.assertNotLost(n: Network? = null) =
+        assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) }
+
+    @Test
+    fun testCallbacks() {
+        // If an interface exists when the callback is registered, it is reported on registration.
+        val iface = createInterface()
+        val listener1 = EthernetStateListener()
+        addInterfaceStateListener(listener1)
+        validateListenerOnRegistration(listener1)
+
+        // If an interface appears, existing callbacks see it.
+        // TODO: fix the up/up/down/up callbacks and only send down/up.
+        val iface2 = createInterface()
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+
+        // Register a new listener, it should see state of all existing interfaces immediately.
+        val listener2 = EthernetStateListener()
+        addInterfaceStateListener(listener2)
+        validateListenerOnRegistration(listener2)
+
+        // Removing interfaces first sends link down, then STATE_ABSENT/ROLE_NONE.
+        removeInterface(iface)
+        for (listener in listOf(listener1, listener2)) {
+            listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+            listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+        }
+
+        removeInterface(iface2)
+        for (listener in listOf(listener1, listener2)) {
+            listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+            listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
+            listener.assertNoCallback()
+        }
+    }
+
+    // TODO: this function is now used in two places (EthernetManagerTest and
+    // EthernetTetheringTest), so it should be moved to testutils.
+    private fun isAdbOverNetwork(): Boolean {
+        // If adb TCP port opened, this test may running by adb over network.
+        return (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1 ||
+                SystemProperties.getInt("service.adb.tcp.port", -1) > -1)
+    }
+
+    @Test
+    fun testCallbacks_forServerModeInterfaces() {
+        // do not run this test when adb might be connected over ethernet.
+        assumeFalse(isAdbOverNetwork())
+
+        val listener = EthernetStateListener()
+        addInterfaceStateListener(listener)
+
+        // it is possible that a physical interface is present, so it is not guaranteed that iface
+        // will be put into server mode. This should not matter for the test though. Calling
+        // createInterface() makes sure we have at least one interface available.
+        val iface = createInterface()
+        val cb = requestTetheredInterface()
+        val ifaceName = cb.expectOnAvailable()
+        listener.eventuallyExpect(ifaceName, STATE_LINK_UP, ROLE_SERVER)
+
+        releaseTetheredInterface()
+        listener.eventuallyExpect(ifaceName, STATE_LINK_UP, ROLE_CLIENT)
+    }
+
+    /**
+     * Validate all interfaces are returned for an EthernetStateListener upon registration.
+     */
+    private fun validateListenerOnRegistration(listener: EthernetStateListener) {
+        // Get all tracked interfaces to validate on listener registration. Ordering and interface
+        // state (up/down) can't be validated for interfaces not created as part of testing.
+        val ifaces = em.getInterfaceList()
+        val polledIfaces = ArraySet<String>()
+        for (i in ifaces) {
+            val event = (listener.pollForNextCallback() as InterfaceStateChanged)
+            val iface = event.iface
+            assertTrue(polledIfaces.add(iface), "Duplicate interface $iface returned")
+            assertTrue(ifaces.contains(iface), "Untracked interface $iface returned")
+            // If the event's iface was created in the test, additional criteria can be validated.
+            createdIfaces.find { it.interfaceName.equals(iface) }?.let {
+                assertEquals(event,
+                    listener.createChangeEvent(it.interfaceName,
+                                                        STATE_LINK_UP,
+                                                        ROLE_CLIENT))
+            }
+        }
+        // Assert all callbacks are accounted for.
+        listener.assertNoCallback()
+    }
+
+    @Test
+    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)
+    }
+
+    @Test
+    fun testNetworkRequest_withSingleExistingInterface() {
+        setIncludeTestInterfaces(true)
+        createInterface()
+
+        // install a listener which will later be used to verify the Lost callback
+        val listenerCb = TestableNetworkCallback()
+        cm.registerNetworkCallback(ETH_REQUEST, listenerCb)
+        networkRequests.add(listenerCb)
+
+        val cb = requestNetwork(ETH_REQUEST)
+        val network = cb.expectAvailable()
+
+        cb.assertNotLost()
+        releaseNetwork(cb)
+        listenerCb.eventuallyExpectLost(network)
+    }
+
+    @Test
+    fun testNetworkRequest_beforeSingleInterfaceIsUp() {
+        setIncludeTestInterfaces(true)
+
+        val cb = requestNetwork(ETH_REQUEST)
+
+        // bring up interface after network has been requested
+        val iface = createInterface()
+        val network = cb.expectAvailable()
+
+        // remove interface before network request has been removed
+        cb.assertNotLost()
+        removeInterface(iface)
+        cb.eventuallyExpectLost()
+
+        releaseNetwork(cb)
+    }
+
+    @Test
+    fun testNetworkRequest_withMultipleInterfaces() {
+        setIncludeTestInterfaces(true)
+
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+
+        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName))
+
+        val network = cb.expectAvailable()
+        cb.expectCapabilitiesThat(network) {
+            it.networkSpecifier == EthernetNetworkSpecifier(iface2.interfaceName)
+        }
+
+        removeInterface(iface1)
+        cb.assertNotLost()
+        removeInterface(iface2)
+        cb.eventuallyExpectLost()
+
+        releaseNetwork(cb)
+    }
+
+    @Test
+    fun testNetworkRequest_withInterfaceBeingReplaced() {
+        setIncludeTestInterfaces(true)
+        val iface1 = createInterface()
+
+        val cb = requestNetwork(ETH_REQUEST)
+        val network = cb.expectAvailable()
+
+        // create another network and verify the request sticks to the current network
+        val iface2 = createInterface()
+        cb.assertNotLost()
+
+        // remove iface1 and verify the request brings up iface2
+        removeInterface(iface1)
+        cb.eventuallyExpectLost(network)
+        val network2 = cb.expectAvailable()
+
+        releaseNetwork(cb)
+    }
+
+    @Test
+    fun testNetworkRequest_withMultipleInterfacesAndRequests() {
+        setIncludeTestInterfaces(true)
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+
+        val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.interfaceName))
+        val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName))
+        val cb3 = requestNetwork(ETH_REQUEST)
+
+        cb1.expectAvailable()
+        cb2.expectAvailable()
+        cb3.expectAvailable()
+
+        cb1.assertNotLost()
+        cb2.assertNotLost()
+        cb3.assertNotLost()
+
+        releaseNetwork(cb1)
+        releaseNetwork(cb2)
+        releaseNetwork(cb3)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java b/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java
new file mode 100644
index 0000000..c8ee0c7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+@RunWith(DevSdkIgnoreRunner.class)
+public class EthernetNetworkUpdateRequestTest {
+    private static final NetworkCapabilities DEFAULT_CAPS =
+            new NetworkCapabilities.Builder()
+                    .removeCapability(NET_CAPABILITY_NOT_RESTRICTED).build();
+    private static final StaticIpConfiguration DEFAULT_STATIC_IP_CONFIG =
+            new StaticIpConfiguration.Builder().setDomains("test").build();
+    private static final IpConfiguration DEFAULT_IP_CONFIG =
+            new IpConfiguration.Builder()
+                    .setStaticIpConfiguration(DEFAULT_STATIC_IP_CONFIG).build();
+
+    private EthernetNetworkUpdateRequest createRequest(@NonNull final NetworkCapabilities nc,
+            @NonNull final IpConfiguration ipConfig) {
+        return new EthernetNetworkUpdateRequest.Builder()
+                .setNetworkCapabilities(nc)
+                .setIpConfiguration(ipConfig)
+                .build();
+    }
+
+    @Test
+    public void testGetNetworkCapabilities() {
+        final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+        assertEquals(DEFAULT_CAPS, r.getNetworkCapabilities());
+    }
+
+    @Test
+    public void testGetIpConfiguration() {
+        final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+        assertEquals(DEFAULT_IP_CONFIG, r.getIpConfiguration());
+    }
+
+    @Test
+    public void testBuilderWithRequest() {
+        final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+        final EthernetNetworkUpdateRequest rFromExisting =
+                new EthernetNetworkUpdateRequest.Builder(r).build();
+
+        assertNotSame(r, rFromExisting);
+        assertEquals(r.getIpConfiguration(), rFromExisting.getIpConfiguration());
+        assertEquals(r.getNetworkCapabilities(), rFromExisting.getNetworkCapabilities());
+    }
+
+    @Test
+    public void testNullIpConfigurationAndNetworkCapabilitiesThrows() {
+        assertThrows("Should not be able to build with null ip config and network capabilities.",
+                IllegalStateException.class,
+                () -> createRequest(null, null));
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6e9f0cd..2b1d173 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -17,16 +17,21 @@
 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 com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+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 +45,34 @@
 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.cts.util.IkeSessionTestUtils;
+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.VpnManagerShimImpl;
+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;
 
@@ -67,6 +82,7 @@
 import java.security.KeyPairGenerator;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
@@ -80,6 +96,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,9 +186,13 @@
     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;
+    private final List<TestableNetworkCallback> mCallbacksToUnregister = new ArrayList<>();
 
     public Ikev2VpnTest() throws Exception {
         // Build certificates
@@ -179,6 +202,9 @@
 
     @After
     public void tearDown() {
+        for (TestableNetworkCallback callback : mCallbacksToUnregister) {
+            sCM.unregisterNetworkCallback(callback);
+        }
         setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
     }
@@ -198,39 +224,61 @@
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileCommon(
-            Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks) throws Exception {
+            @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
+            boolean requiresValidation) throws Exception {
+
+        builder.setBypassable(true)
+                .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
+                .setProxy(TEST_PROXY_INFO)
+                .setMaxMtu(TEST_MTU)
+                .setMetered(false);
+        if (TestUtils.shouldTestTApis()) {
+            builder.setRequiresInternetValidation(requiresValidation);
+        }
         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 buildIkev2VpnProfileIkeTunConnParams(
+            final boolean isRestrictedToTestNetworks, final boolean requiresValidation,
+            final boolean testIpv6) throws Exception {
+        final IkeTunnelConnectionParams params =
+                new IkeTunnelConnectionParams(testIpv6
+                        ? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
+                        IkeSessionTestUtils.CHILD_PARAMS);
+
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(params)
+                        .setRequiresInternetValidation(requiresValidation)
+                        .setProxy(TEST_PROXY_INFO)
+                        .setMaxMtu(TEST_MTU)
+                        .setMetered(false);
+
+        if (isRestrictedToTestNetworks) {
+            builder.restrictToTestNetworks();
+        }
+        return builder.build();
     }
 
-    private Ikev2VpnProfile buildIkev2VpnProfilePsk(
-            String remote, boolean isRestrictedToTestNetworks) throws Exception {
+    private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
+            boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
         final Ikev2VpnProfile.Builder builder =
                 new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
-
-        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+        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);
+        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
+                false /* requiresValidation */);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
@@ -239,8 +287,8 @@
                 new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .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 +302,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 +317,38 @@
         assertNull(profile.getServerRootCaCert());
         assertNull(profile.getRsaPrivateKey());
         assertNull(profile.getUserCert());
+        if (isAtLeastT()) {
+            assertEquals(requiresValidation, profile.isInternetValidationRequired());
+        }
+    }
+
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testBuildIkev2VpnProfileWithIkeTunnelConnectionParams() throws Exception {
+        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+        assumeTrue(TestUtils.shouldTestTApis());
+
+        final IkeTunnelConnectionParams expectedParams = new IkeTunnelConnectionParams(
+                IkeSessionTestUtils.IKE_PARAMS_V6, IkeSessionTestUtils.CHILD_PARAMS);
+        final Ikev2VpnProfile.Builder ikeProfileBuilder =
+                new Ikev2VpnProfile.Builder(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 = ikeProfileBuilder.build();
+
+        assertEquals(expectedParams, profile.getIkeTunnelConnectionParams());
+    }
+
+    @Test
+    public void testBuildIkev2VpnProfilePsk() throws Exception {
+        doTestBuildIkev2VpnProfilePsk(true /* requiresValidation */);
+        doTestBuildIkev2VpnProfilePsk(false /* requiresValidation */);
     }
 
     @Test
@@ -316,8 +395,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 +439,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 +473,9 @@
         }
     }
 
-    private void checkStartStopVpnProfileBuildsNetworks(IkeTunUtils tunUtils, boolean testIpv6)
+    private void checkStartStopVpnProfileBuildsNetworks(@NonNull IkeTunUtils tunUtils,
+            boolean testIpv6, boolean requiresValidation, boolean testSessionKey,
+            boolean testIkeTunConnParams)
             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 +485,32 @@
         // 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 = testIkeTunConnParams
+                ? buildIkev2VpnProfileIkeTunConnParams(true /* isRestrictedToTestNetworks */,
+                        requiresValidation, testIpv6)
+                : 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();
+        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 +520,71 @@
                 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. However, there is a race in Vpn(see b/228574221) that VPN may
+        // misuse VPN network itself as the underlying network. The fix is not available without
+        // SDK > T platform. Thus, verify this only on T+ platform.
+        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 void registerNetworkCallback(NetworkRequest request, TestableNetworkCallback callback) {
+        sCM.registerNetworkCallback(request, callback);
+        mCallbacksToUnregister.add(callback);
     }
 
     private class VerifyStartStopVpnProfileTest implements TestNetworkRunnable.Test {
         private final boolean mTestIpv6Only;
+        private final boolean mRequiresValidation;
+        private final boolean mTestSessionKey;
+        private final boolean mTestIkeTunConnParams;
 
         /**
          * 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, boolean testIkeTunConnParams) {
             mTestIpv6Only = testIpv6Only;
+            mRequiresValidation = requiresValidation;
+            mTestSessionKey = testSessionKey;
+            mTestIkeTunConnParams = testIkeTunConnParams;
         }
 
         @Override
@@ -454,7 +592,8 @@
                 throws Exception {
             final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
 
-            checkStartStopVpnProfileBuildsNetworks(tunUtils, mTestIpv6Only);
+            checkStartStopVpnProfileBuildsNetworks(tunUtils, mTestIpv6Only, mRequiresValidation,
+                    mTestSessionKey, mTestIkeTunConnParams);
         }
 
         @Override
@@ -472,22 +611,83 @@
         }
     }
 
-    @Test
-    public void testStartStopVpnProfileV4() throws Exception {
+    private void doTestStartStopVpnProfile(boolean testIpv6Only, boolean requiresValidation,
+            boolean testSessionKey, boolean testIkeTunConnParams) throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
-
         // Requires shell permission to update appops.
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
+    }
+
+    @Test
+    public void testStartStopVpnProfileV4() throws Exception {
+        doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+                false /* testSessionKey */, false /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileV4WithValidation() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
+                false /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
     @Test
     public void testStartStopVpnProfileV6() throws Exception {
-        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+                false /* testSessionKey */, false /* testIkeTunConnParams */);
+    }
 
-        // Requires shell permission to update appops.
-        runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true)));
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileV6WithValidation() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
+                false /* testSessionKey */, false /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileIkeTunConnParamsV4() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+                false /* testSessionKey */, true /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileIkeTunConnParamsV4WithValidation() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
+                false /* testSessionKey */, true /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileIkeTunConnParamsV6() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+                false /* testSessionKey */, true /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileIkeTunConnParamsV6WithValidation() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
+                false /* testSessionKey */, true /* testIkeTunConnParams */);
+    }
+
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testStartProvisionedVpnV4ProfileSession() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+                true /* testSessionKey */, false /* testIkeTunConnParams */);
+    }
+
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testStartProvisionedVpnV6ProfileSession() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+                true /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 53b00db..d4f3d57 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -72,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
@@ -106,7 +107,6 @@
 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
@@ -946,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()
@@ -975,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 ->
@@ -1023,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 ->
@@ -1064,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()
@@ -1107,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 {
@@ -1151,8 +1141,9 @@
         )
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
-    fun testDestroyAndAwaitReplacement() {
+    fun testUnregisterAfterReplacement() {
         // Keeps an eye on all test networks.
         val matchAllCallback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
         registerNetworkCallback(makeTestNetworkRequest(), matchAllCallback)
@@ -1180,15 +1171,15 @@
         // 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.destroyAndAwaitReplacement(5_000 /* timeoutMillis */)
+        agent1.unregisterAfterReplacement(5_000 /* timeoutMillis */)
         agent1.expectCallback<OnNetworkDestroyed>()
         assertThrows(IOException::class.java) { network1.bindSocket(DatagramSocket()) }
         assertNotNull(mCM.getLinkProperties(network1))
 
-        // Calling destroyAndAwaitReplacement more than once has no effect.
+        // 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.destroyAndAwaitReplacement(1 /* timeoutMillis */)
+        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).
@@ -1216,14 +1207,14 @@
         matchAllCallback.expectCallback<Losing>(network3)
         testCallback.expectAvailableCallbacks(network4, validated = true)
         mCM.unregisterNetworkCallback(agent4callback)
-        agent3.destroyAndAwaitReplacement(5_000)
+        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.destroyAndAwaitReplacement(100 /* timeoutMillis */)
+        agent4.unregisterAfterReplacement(100 /* timeoutMillis */)
         matchAllCallback.expectCallback<Lost>(network4, 1000L /* timeoutMs */)
         testCallback.expectCallback<Lost>(network4, 1000L /* timeoutMs */)
         agent4.expectCallback<OnNetworkDestroyed>()
@@ -1234,7 +1225,7 @@
         val (agent5, network5) = connectNetwork()
         matchAllCallback.expectAvailableThenValidatedCallbacks(network5)
         testCallback.expectAvailableThenValidatedCallbacks(network5)
-        agent5.destroyAndAwaitReplacement(5_000 /* timeoutMillis */)
+        agent5.unregisterAfterReplacement(5_000 /* timeoutMillis */)
         agent5.unregister()
         matchAllCallback.expectCallback<Lost>(network5, 1000L /* timeoutMs */)
         testCallback.expectCallback<Lost>(network5, 1000L /* timeoutMs */)
@@ -1257,7 +1248,7 @@
             it.hasCapability(NET_CAPABILITY_VALIDATED)
         }
 
-        wifiAgent.destroyAndAwaitReplacement(5_000 /* timeoutMillis */)
+        wifiAgent.unregisterAfterReplacement(5_000 /* timeoutMillis */)
         wifiAgent.expectCallback<OnNetworkDestroyed>()
 
         // Once the network is awaiting replacement, changing LinkProperties, NetworkCapabilities or
@@ -1284,4 +1275,23 @@
         matchAllCallback.expectCallback<Lost>(wifiNetwork)
         wifiAgent.expectCallback<OnNetworkUnwanted>()
     }
+
+    @Test
+    fun testUnregisterAgentBeforeAgentFullyConnected() {
+        val specifier = UUID.randomUUID().toString()
+        val callback = TestableNetworkCallback()
+        val transports = intArrayOf(TRANSPORT_CELLULAR)
+        // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
+        requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+        val nc = makeTestNetworkCapabilities(specifier, transports)
+        val agent = createNetworkAgent(realContext, initialNc = nc)
+        // Connect the agent
+        agent.register()
+        // Mark agent connected then unregister agent immediately. Verify that both available and
+        // lost callback should be sent still.
+        agent.markConnected()
+        agent.unregister()
+        callback.expectCallback<Available>(agent.network!!)
+        callback.eventuallyExpect<Lost> { it.network == agent.network }
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index fb720a7..f86c5cd 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -29,8 +29,20 @@
 import static android.app.usage.NetworkStats.Bucket.STATE_FOREGROUND;
 import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
 import static android.app.usage.NetworkStats.Bucket.UID_ALL;
+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 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.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.AppOpsManager;
+import android.app.Instrumentation;
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
@@ -40,7 +52,11 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.NetworkRequest;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
 import android.net.TrafficStats;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
@@ -48,11 +64,23 @@
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
 import android.telephony.TelephonyManager;
-import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.ConnectivityModuleTest;
+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 java.io.IOException;
 import java.io.InputStream;
@@ -62,8 +90,18 @@
 import java.net.UnknownHostException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
-public class NetworkStatsManagerTest extends InstrumentationTestCase {
+@ConnectivityModuleTest
+@AppModeFull(reason = "instant apps cannot be granted USAGE_STATS")
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsManagerTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(Build.VERSION_CODES.Q);
+
     private static final String LOG_TAG = "NetworkStatsManagerTest";
     private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}";
     private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}";
@@ -164,9 +202,11 @@
             };
 
     private String mPkg;
+    private Context mContext;
     private NetworkStatsManager mNsm;
     private ConnectivityManager mCm;
     private PackageManager mPm;
+    private Instrumentation mInstrumentation;
     private long mStartTime;
     private long mEndTime;
 
@@ -224,44 +264,40 @@
         }
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mNsm = (NetworkStatsManager) getInstrumentation().getContext()
-                .getSystemService(Context.NETWORK_STATS_SERVICE);
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mNsm = mContext.getSystemService(NetworkStatsManager.class);
         mNsm.setPollForce(true);
 
-        mCm = (ConnectivityManager) getInstrumentation().getContext()
-                .getSystemService(Context.CONNECTIVITY_SERVICE);
+        mCm = mContext.getSystemService(ConnectivityManager.class);
+        mPm = mContext.getPackageManager();
+        mPkg = mContext.getPackageName();
 
-        mPm = getInstrumentation().getContext().getPackageManager();
-
-        mPkg = getInstrumentation().getContext().getPackageName();
-
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mWriteSettingsMode = getAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS);
         setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, "allow");
         mUsageStatsMode = getAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mWriteSettingsMode != null) {
             setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, mWriteSettingsMode);
         }
         if (mUsageStatsMode != null) {
             setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, mUsageStatsMode);
         }
-        super.tearDown();
     }
 
     private void setAppOpsMode(String appop, String mode) throws Exception {
         final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode);
-        SystemUtil.runShellCommand(command);
+        SystemUtil.runShellCommand(mInstrumentation, command);
     }
 
     private String getAppOpsMode(String appop) throws Exception {
         final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop);
-        String result = SystemUtil.runShellCommand(command);
+        String result = SystemUtil.runShellCommand(mInstrumentation, command);
         if (result == null) {
             Log.w(LOG_TAG, "App op " + appop + " could not be read.");
         }
@@ -269,7 +305,7 @@
     }
 
     private boolean isInForeground() throws IOException {
-        String result = SystemUtil.runShellCommand(getInstrumentation(),
+        String result = SystemUtil.runShellCommand(mInstrumentation,
                 "cmd activity get-uid-state " + Process.myUid());
         return result.contains("FOREGROUND");
     }
@@ -366,15 +402,14 @@
     private String getSubscriberId(int networkIndex) {
         int networkType = mNetworkInterfacesToTest[networkIndex].getNetworkType();
         if (ConnectivityManager.TYPE_MOBILE == networkType) {
-            TelephonyManager tm = (TelephonyManager) getInstrumentation().getContext()
-                    .getSystemService(Context.TELEPHONY_SERVICE);
+            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
             return ShellIdentityUtils.invokeMethodWithShellPermissions(tm,
                     (telephonyManager) -> telephonyManager.getSubscriberId());
         }
         return "";
     }
 
-    @AppModeFull
+    @Test
     public void testDeviceSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -410,7 +445,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testUserSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -446,7 +481,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testAppSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Use tolerance value that large enough to make sure stats of at
@@ -522,7 +557,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testAppDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -565,7 +600,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testUidDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -619,7 +654,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testTagDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -726,7 +761,7 @@
                 bucket.getRxBytes(), bucket.getTxBytes()));
     }
 
-    @AppModeFull
+    @Test
     public void testUidTagStateDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -803,7 +838,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testCallback() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -825,6 +860,43 @@
             // storing files of >2MB in CTS.
 
             mNsm.unregisterUsageCallback(usageCallback);
+
+            // For T- devices, the registerUsageCallback invocation below will need a looper
+            // from the thread that calls into the API, which is not available in the test.
+            if (SdkLevel.isAtLeastT()) {
+                mNsm.registerUsageCallback(mNetworkInterfacesToTest[i].getNetworkType(),
+                        getSubscriberId(i), THRESHOLD_BYTES, usageCallback);
+                mNsm.unregisterUsageCallback(usageCallback);
+            }
+        }
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @Test
+    public void testDataMigrationUtils() throws Exception {
+        final List<String> prefixes = List.of(PREFIX_UID, PREFIX_XT, PREFIX_UID_TAG);
+        for (final String prefix : prefixes) {
+            final long duration = TextUtils.equals(PREFIX_XT, prefix) ? TimeUnit.HOURS.toMillis(1)
+                    : TimeUnit.HOURS.toMillis(2);
+
+            final NetworkStatsCollection collection =
+                    NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, duration);
+
+            final long now = System.currentTimeMillis();
+            final Set<Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory>> entries =
+                    collection.getEntries().entrySet();
+            for (final Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory> entry : entries) {
+                for (final NetworkStatsHistory.Entry historyEntry : entry.getValue().getEntries()) {
+                    // Verify all value fields are reasonable.
+                    assertTrue(historyEntry.getBucketStart() <= now);
+                    assertTrue(historyEntry.getActiveTime() <= duration);
+                    assertTrue(historyEntry.getRxBytes() >= 0);
+                    assertTrue(historyEntry.getRxPackets() >= 0);
+                    assertTrue(historyEntry.getTxBytes() >= 0);
+                    assertTrue(historyEntry.getTxPackets() >= 0);
+                    assertTrue(historyEntry.getOperations() >= 0);
+                }
+            }
         }
     }
 
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 9506081..64cc97d 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,6 +22,7 @@
 import android.net.Network
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkRequest
@@ -45,17 +46,16 @@
 import android.net.nsd.NsdManager.RegistrationListener
 import android.net.nsd.NsdManager.ResolveListener
 import android.net.nsd.NsdServiceInfo
+import android.os.Handler
 import android.os.HandlerThread
+import android.os.Process.myTid
 import android.platform.test.annotations.AppModeFull
 import android.util.Log
 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.networkstack.apishim.ConstantsShim
 import com.android.networkstack.apishim.NsdShimImpl
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.SC_V2
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.runAsShell
@@ -65,12 +65,12 @@
 import org.junit.Assert.assertTrue
 import org.junit.Assume.assumeTrue
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 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
@@ -81,6 +81,7 @@
 private const val TAG = "NsdManagerTest"
 private const val SERVICE_TYPE = "_nmt._tcp"
 private const val TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
 private const val DBG = false
 
 private val nsdShim = NsdShimImpl.newInstance()
@@ -88,10 +89,6 @@
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 @RunWith(AndroidJUnit4::class)
 class NsdManagerTest {
-    // NsdManager is not updatable before S, 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 nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
 
@@ -117,12 +114,20 @@
 
     private interface NsdEvent
     private open class NsdRecord<T : NsdEvent> private constructor(
-        private val history: ArrayTrackRecord<T>
+        private val history: ArrayTrackRecord<T>,
+        private val expectedThreadId: Int? = null
     ) : TrackRecord<T> by history {
-        constructor() : this(ArrayTrackRecord())
+        constructor(expectedThreadId: Int? = null) : this(ArrayTrackRecord(), expectedThreadId)
 
         val nextEvents = history.newReadHead()
 
+        override fun add(e: T): Boolean {
+            if (expectedThreadId != null) {
+                assertEquals(expectedThreadId, myTid(), "Callback is running on the wrong thread")
+            }
+            return history.add(e)
+        }
+
         inline fun <reified V : NsdEvent> expectCallbackEventually(
             crossinline predicate: (V) -> Boolean = { true }
         ): V = nextEvents.poll(TIMEOUT_MS) { e -> e is V && predicate(e) } as V?
@@ -135,10 +140,15 @@
                     nextEvent.javaClass.simpleName)
             return nextEvent
         }
+
+        inline fun assertNoCallback(timeoutMs: Long = NO_CALLBACK_TIMEOUT_MS) {
+            val cb = nextEvents.poll(timeoutMs)
+            assertNull(cb, "Expected no callback but got $cb")
+        }
     }
 
-    private class NsdRegistrationRecord : RegistrationListener,
-            NsdRecord<NsdRegistrationRecord.RegistrationEvent>() {
+    private class NsdRegistrationRecord(expectedThreadId: Int? = null) : RegistrationListener,
+            NsdRecord<NsdRegistrationRecord.RegistrationEvent>(expectedThreadId) {
         sealed class RegistrationEvent : NsdEvent {
             abstract val serviceInfo: NsdServiceInfo
 
@@ -175,8 +185,8 @@
         }
     }
 
-    private class NsdDiscoveryRecord : DiscoveryListener,
-            NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>() {
+    private class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
+            DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
         sealed class DiscoveryEvent : NsdEvent {
             data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int)
                 : DiscoveryEvent()
@@ -248,9 +258,11 @@
     fun setUp() {
         handlerThread.start()
 
-        runAsShell(MANAGE_TEST_NETWORKS) {
-            testNetwork1 = createTestNetwork()
-            testNetwork2 = createTestNetwork()
+        if (TestUtils.shouldTestTApis()) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                testNetwork1 = createTestNetwork()
+                testNetwork2 = createTestNetwork()
+            }
         }
     }
 
@@ -289,9 +301,11 @@
 
     @After
     fun tearDown() {
-        runAsShell(MANAGE_TEST_NETWORKS) {
-            testNetwork1.close(cm)
-            testNetwork2.close(cm)
+        if (TestUtils.shouldTestTApis()) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                testNetwork1.close(cm)
+                testNetwork2.close(cm)
+            }
         }
         handlerThread.quitSafely()
     }
@@ -342,9 +356,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 +370,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)
@@ -386,14 +406,17 @@
         si2.serviceName = serviceName
         si2.port = localPort
         val registrationRecord2 = NsdRegistrationRecord()
-        val registeredInfo2 = registerService(registrationRecord2, si2)
+        nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
+        val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>().serviceInfo
 
         // Expect a service record to be discovered (and filter the ones
         // that are unrelated to this test)
         val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
 
         // Resolve the service
-        val resolvedService2 = resolveService(foundInfo2)
+        val resolveRecord2 = NsdResolveRecord()
+        nsdManager.resolveService(foundInfo2, resolveRecord2)
+        val resolvedService2 = resolveRecord2.expectCallback<ServiceResolved>().serviceInfo
 
         // Check that the resolved service doesn't have any TXT records
         assertEquals(0, resolvedService2.attributes.size)
@@ -408,8 +431,8 @@
 
     @Test
     fun testNsdManager_DiscoverOnNetwork() {
-        // This tests requires shims supporting T+ APIs (discovering on specific network)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        // This test requires shims supporting T+ APIs (discovering on specific network)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
@@ -422,7 +445,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,17 +465,20 @@
 
     @Test
     fun testNsdManager_DiscoverWithNetworkRequest() {
-        // This tests requires shims supporting T+ APIs (discovering on network request)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        // This test requires shims supporting T+ APIs (discovering on network request)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
         si.serviceName = this.serviceName
         si.port = 12345 // Test won't try to connect so port does not matter
 
-        val registrationRecord = NsdRegistrationRecord()
-        val registeredInfo1 = registerService(registrationRecord, si)
-        val discoveryRecord = NsdDiscoveryRecord()
+        val handler = Handler(handlerThread.looper)
+        val executor = Executor { handler.post(it) }
+
+        val registrationRecord = NsdRegistrationRecord(expectedThreadId = handlerThread.threadId)
+        val registeredInfo1 = registerService(registrationRecord, si, executor)
+        val discoveryRecord = NsdDiscoveryRecord(expectedThreadId = handlerThread.threadId)
 
         tryTest {
             val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
@@ -462,7 +488,7 @@
                             .addTransportType(TRANSPORT_TEST)
                             .setNetworkSpecifier(specifier)
                             .build(),
-                    discoveryRecord)
+                    executor, discoveryRecord)
 
             val discoveryStarted = discoveryRecord.expectCallback<DiscoveryStarted>()
             assertEquals(SERVICE_TYPE, discoveryStarted.serviceType)
@@ -478,7 +504,7 @@
             assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost1.serviceInfo))
 
             registrationRecord.expectCallback<ServiceUnregistered>()
-            val registeredInfo2 = registerService(registrationRecord, si)
+            val registeredInfo2 = registerService(registrationRecord, si, executor)
             val serviceDiscovered2 = discoveryRecord.expectCallback<ServiceFound>()
             assertEquals(registeredInfo2.serviceName, serviceDiscovered2.serviceInfo.serviceName)
             assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered2.serviceInfo))
@@ -506,9 +532,42 @@
     }
 
     @Test
+    fun testNsdManager_DiscoverWithNetworkRequest_NoMatchingNetwork() {
+        // This test requires shims supporting T+ APIs (discovering on network request)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = this.serviceName
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        val handler = Handler(handlerThread.looper)
+        val executor = Executor { handler.post(it) }
+
+        val discoveryRecord = NsdDiscoveryRecord(expectedThreadId = handlerThread.threadId)
+        val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
+
+        tryTest {
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                    NetworkRequest.Builder()
+                            .removeCapability(NET_CAPABILITY_TRUSTED)
+                            .addTransportType(TRANSPORT_TEST)
+                            // Specified network does not have this capability
+                            .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+                            .setNetworkSpecifier(specifier)
+                            .build(),
+                    executor, discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStarted>()
+        } cleanup {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        }
+    }
+
+    @Test
     fun testNsdManager_ResolveOnNetwork() {
-        // This tests requires shims supporting T+ APIs (NsdServiceInfo.network)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
@@ -532,7 +591,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
@@ -549,11 +608,99 @@
         }
     }
 
+    @Test
+    fun testNsdManager_RegisterOnNetwork() {
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = this.serviceName
+        si.network = testNetwork1.network
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        registerService(registrationRecord, si)
+        val discoveryRecord = NsdDiscoveryRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        val discoveryRecord3 = NsdDiscoveryRecord()
+
+        tryTest {
+            // Discover service on testNetwork1.
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network, Executor { it.run() }, discoveryRecord)
+            // Expect that service is found on testNetwork1
+            val foundInfo = discoveryRecord.waitForServiceDiscovered(
+                serviceName, testNetwork1.network)
+            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
+
+            // Discover service on testNetwork2.
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork2.network, Executor { it.run() }, discoveryRecord2)
+            // Expect that discovery is started then no other callbacks.
+            discoveryRecord2.expectCallback<DiscoveryStarted>()
+            discoveryRecord2.assertNoCallback()
+
+            // Discover service on all networks (not specify any network).
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                null as Network? /* network */, Executor { it.run() }, discoveryRecord3)
+            // Expect that service is found on testNetwork1
+            val foundInfo3 = discoveryRecord3.waitForServiceDiscovered(
+                    serviceName, testNetwork1.network)
+            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo3))
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+            discoveryRecord2.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testNsdManager_RegisterServiceNameWithNonStandardCharacters() {
+        val serviceNames = "^Nsd.Test|Non-#AsCiI\\Characters&\\ufffe テスト 測試"
+        val si = NsdServiceInfo().apply {
+            serviceType = SERVICE_TYPE
+            serviceName = serviceNames
+            port = 12345 // Test won't try to connect so port does not matter
+        }
+
+        // Register the service name which contains non-standard characters.
+        val registrationRecord = NsdRegistrationRecord()
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+        registrationRecord.expectCallback<ServiceRegistered>()
+
+        tryTest {
+            // Discover that service name.
+            val discoveryRecord = NsdDiscoveryRecord()
+            nsdManager.discoverServices(
+                SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord
+            )
+            val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames)
+
+            // Expect that resolving the service name works properly even service name contains
+            // non-standard characters.
+            val resolveRecord = NsdResolveRecord()
+            nsdManager.resolveService(foundInfo, resolveRecord)
+            val resolvedCb = resolveRecord.expectCallback<ServiceResolved>()
+            assertEquals(foundInfo.serviceName, resolvedCb.serviceInfo.serviceName)
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+        } cleanup {
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
     /**
      * Register a service and return its registration record.
      */
-    private fun registerService(record: NsdRegistrationRecord, si: NsdServiceInfo): NsdServiceInfo {
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, record)
+    private fun registerService(
+        record: NsdRegistrationRecord,
+        si: NsdServiceInfo,
+        executor: Executor = Executor { it.run() }
+    ): NsdServiceInfo {
+        nsdShim.registerService(nsdManager, si, NsdManager.PROTOCOL_DNS_SD, executor, 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 +709,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/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..244bfc5
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
@@ -0,0 +1,88 @@
+/*
+ * 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.DH_GROUP_4096_BIT_MODP;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128;
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_128;
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_256;
+import static android.net.ipsec.ike.SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC;
+
+import android.net.InetAddresses;
+import android.net.ipsec.ike.ChildSaProposal;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeIpv4AddrIdentification;
+import android.net.ipsec.ike.IkeIpv6AddrIdentification;
+import android.net.ipsec.ike.IkeSaProposal;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.TunnelModeChildSessionParams;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/** Shared testing parameters and util methods for testing IKE */
+public class IkeSessionTestUtils {
+    private static final String TEST_SERVER_ADDR_V4 = "192.0.2.2";
+    private static final String TEST_SERVER_ADDR_V6 = "2001:db8::2";
+    private static final String TEST_IDENTITY = "client.cts.android.com";
+    private static final byte[] TEST_PSK = "ikeAndroidPsk".getBytes();
+    public static final IkeSessionParams IKE_PARAMS_V4 = getTestIkeSessionParams(false);
+    public static final IkeSessionParams IKE_PARAMS_V6 = getTestIkeSessionParams(true);
+
+    public static final TunnelModeChildSessionParams CHILD_PARAMS = getChildSessionParams();
+
+    private static TunnelModeChildSessionParams getChildSessionParams() {
+        final TunnelModeChildSessionParams.Builder childOptionsBuilder =
+                new TunnelModeChildSessionParams.Builder()
+                        .addSaProposal(getChildSaProposals());
+
+        return childOptionsBuilder.build();
+    }
+
+    private static IkeSessionParams getTestIkeSessionParams(boolean testIpv6) {
+        final String testServer = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
+        final InetAddress addr = InetAddresses.parseNumericAddress(testServer);
+        final IkeSessionParams.Builder ikeOptionsBuilder =
+                new IkeSessionParams.Builder()
+                        .setServerHostname(testServer)
+                        .setLocalIdentification(new IkeFqdnIdentification(TEST_IDENTITY))
+                        .setRemoteIdentification(testIpv6
+                                ? new IkeIpv6AddrIdentification((Inet6Address) addr)
+                                : new IkeIpv4AddrIdentification((Inet4Address) addr))
+                        .setAuthPsk(TEST_PSK)
+                        .addSaProposal(getIkeSaProposals());
+
+        return ikeOptionsBuilder.build();
+    }
+
+    private static IkeSaProposal getIkeSaProposals() {
+        return new IkeSaProposal.Builder()
+                .addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256)
+                .addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128)
+                .addDhGroup(DH_GROUP_4096_BIT_MODP)
+                .addPseudorandomFunction(PSEUDORANDOM_FUNCTION_AES128_XCBC).build();
+    }
+
+    private static ChildSaProposal getChildSaProposals() {
+        return new ChildSaProposal.Builder()
+                .addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_128)
+                .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/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index e9c4e5a..6096a8b 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -56,7 +56,7 @@
     defaults: ["CtsTetheringTestDefaults"],
 
     min_sdk_version: "30",
-    target_sdk_version: "30",
+    target_sdk_version: "33",
 
     static_libs: [
         "TetheringIntegrationTestsLatestSdkLib",
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/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 2bba282..db39e6f 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -42,6 +42,9 @@
 
 #define PLATFORM "/sys/fs/bpf/"
 #define TETHERING "/sys/fs/bpf/tethering/"
+#define PRIVATE "/sys/fs/bpf/net_private/"
+#define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD "/sys/fs/bpf/netd_shared/"
 
 class BpfExistenceTest : public ::testing::Test {
 };
@@ -84,6 +87,45 @@
 };
 
 static const set<string> INTRODUCED_T = {
+    SHARED "map_block_blocked_ports_map",
+    SHARED "map_clatd_clat_egress4_map",
+    SHARED "map_clatd_clat_ingress6_map",
+    SHARED "map_dscp_policy_ipv4_dscp_policies_map",
+    SHARED "map_dscp_policy_ipv4_socket_to_policies_map_A",
+    SHARED "map_dscp_policy_ipv4_socket_to_policies_map_B",
+    SHARED "map_dscp_policy_ipv6_dscp_policies_map",
+    SHARED "map_dscp_policy_ipv6_socket_to_policies_map_A",
+    SHARED "map_dscp_policy_ipv6_socket_to_policies_map_B",
+    SHARED "map_dscp_policy_switch_comp_map",
+    NETD "map_netd_app_uid_stats_map",
+    NETD "map_netd_configuration_map",
+    NETD "map_netd_cookie_tag_map",
+    NETD "map_netd_iface_index_name_map",
+    NETD "map_netd_iface_stats_map",
+    NETD "map_netd_stats_map_A",
+    NETD "map_netd_stats_map_B",
+    NETD "map_netd_uid_counterset_map",
+    NETD "map_netd_uid_owner_map",
+    NETD "map_netd_uid_permission_map",
+    SHARED "prog_clatd_schedcls_egress4_clat_ether",
+    SHARED "prog_clatd_schedcls_egress4_clat_rawip",
+    SHARED "prog_clatd_schedcls_ingress6_clat_ether",
+    SHARED "prog_clatd_schedcls_ingress6_clat_rawip",
+    NETD "prog_netd_cgroupskb_egress_stats",
+    NETD "prog_netd_cgroupskb_ingress_stats",
+    NETD "prog_netd_cgroupsock_inet_create",
+    NETD "prog_netd_schedact_ingress_account",
+    NETD "prog_netd_skfilter_allowlist_xtbpf",
+    NETD "prog_netd_skfilter_denylist_xtbpf",
+    NETD "prog_netd_skfilter_egress_xtbpf",
+    NETD "prog_netd_skfilter_ingress_xtbpf",
+};
+
+static const set<string> INTRODUCED_T_5_4 = {
+    SHARED "prog_block_bind4_block_port",
+    SHARED "prog_block_bind6_block_port",
+    SHARED "prog_dscp_policy_schedcls_set_dscp_ether",
+    SHARED "prog_dscp_policy_schedcls_set_dscp_raw_ip",
 };
 
 static const set<string> REMOVED_T = {
@@ -125,6 +167,7 @@
 
     if (IsAtLeastT()) {
         addAll(expected, INTRODUCED_T);
+        if (android::bpf::isAtLeastKernelVersion(5, 4, 0)) addAll(expected, INTRODUCED_T_5_4);
         removeAll(expected, REMOVED_T);
 
         addAll(unexpected, REMOVED_T);
diff --git a/tests/native/Android.bp b/tests/native/Android.bp
new file mode 100644
index 0000000..7d43aa8
--- /dev/null
+++ b/tests/native/Android.bp
@@ -0,0 +1,40 @@
+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",
+}
+
+filegroup {
+    name: "net_native_test_config_template",
+    srcs: [
+        "NetNativeTestConfigTemplate.xml",
+    ],
+}
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/NetNativeTestConfigTemplate.xml b/tests/native/NetNativeTestConfigTemplate.xml
new file mode 100644
index 0000000..b71e9aa
--- /dev/null
+++ b/tests/native/NetNativeTestConfigTemplate.xml
@@ -0,0 +1,31 @@
+<!-- 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 {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" />
+    <!-- Only run tests if the device under test is SDK version 33 (Android 13) or above. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
+
+    <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}" />
+        <option name="append-bitness" value="true" />
+    </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..3db5265
--- /dev/null
+++ b/tests/native/connectivity_native_test.cpp
@@ -0,0 +1,286 @@
+/*
+ * 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 <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+#include <android-modules-utils/sdk_level.h>
+#include <cutils/misc.h>  // FIRST_APPLICATION_UID
+#include <gtest/gtest.h>
+#include <netinet/in.h>
+
+#include "bpf/BpfUtils.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.";
+
+        // Skip test case if not on 5.4 kernel which is required by bpf prog.
+        if (!android::bpf::isAtLeastKernelVersion(5, 4, 0)) GTEST_SKIP() <<
+                "Kernel should be at least 5.4.";
+
+        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/unit/Android.bp b/tests/unit/Android.bp
index 07dcae3..3ea27f7 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -54,39 +54,21 @@
 filegroup {
     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",
-        "java/android/net/IpSecConfigTest.java",
-        "java/android/net/IpSecManagerTest.java",
-        "java/android/net/IpSecTransformTest.java",
-        "java/android/net/KeepalivePacketDataUtilTest.java",
-        "java/android/net/NetworkIdentityTest.kt",
-        "java/android/net/NetworkStats*.java",
-        "java/android/net/NetworkTemplateTest.kt",
         "java/android/net/TelephonyNetworkSpecifierTest.java",
         "java/android/net/VpnManagerTest.java",
         "java/android/net/ipmemorystore/*.java",
         "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
-        "java/android/net/nsd/*.java",
         "java/com/android/internal/net/NetworkUtilsInternalTest.java",
         "java/com/android/internal/net/VpnProfileTest.java",
-        "java/com/android/server/IpSecServiceParameterizedTest.java",
-        "java/com/android/server/IpSecServiceRefcountedResourceTest.java",
-        "java/com/android/server/IpSecServiceTest.java",
         "java/com/android/server/NetworkManagementServiceTest.java",
-        "java/com/android/server/NsdServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
         "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/net/ipmemorystore/*.java",
-        "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
-        "java/com/android/server/net/NetworkStats*.java",
-        "java/com/android/server/net/TestableUsageCallback.kt",
     ]
 }
 
@@ -105,7 +87,7 @@
     name: "FrameworksNetTestsDefaults",
     min_sdk_version: "30",
     defaults: [
-        "framework-connectivity-test-defaults",
+        "framework-connectivity-internal-test-defaults",
     ],
     srcs: [
         "java/**/*.java",
@@ -128,6 +110,7 @@
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
         "services.core-vpn",
+        "cts-net-utils"
     ],
     libs: [
         "android.net.ipsec.ike.stubs.module_lib",
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 887f171..54e1cd0 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -50,7 +50,7 @@
     <uses-permission android:name="android.permission.NETWORK_STATS_PROVIDER" />
     <uses-permission android:name="android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE" />
 
-    <application>
+    <application android:testOnly="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="android.net.ipsec.ike" />
         <activity
diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml
index 939ae49..2d32e55 100644
--- a/tests/unit/AndroidTest.xml
+++ b/tests/unit/AndroidTest.xml
@@ -15,7 +15,8 @@
 -->
 <configuration description="Runs Frameworks Networking Tests.">
     <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
-        <option name="test-file-name" value="FrameworksNetTests.apk" />
+      <option name="test-file-name" value="FrameworksNetTests.apk" />
+      <option name="install-arg" value="-t" />
     </target_preparer>
 
     <option name="test-suite-tag" value="apct" />
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
index 561e621..b1b76ec 100644
--- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -54,7 +54,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NetworkStatsManagerTest {
     private static final String TEST_SUBSCRIBER_ID = "subid";
 
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index f324630..c327868 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -41,6 +41,7 @@
 
 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 static org.mockito.ArgumentMatchers.anyBoolean;
@@ -72,6 +73,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -82,6 +84,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.lang.ref.WeakReference;
+
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
@@ -461,4 +465,49 @@
         }
         fail("expected exception of type " + throwableType);
     }
+
+    private static class MockContext extends BroadcastInterceptingContext {
+        MockContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public Context getApplicationContext() {
+            return mock(Context.class);
+        }
+    }
+
+    private WeakReference<Context> makeConnectivityManagerAndReturnContext() {
+        // Mockito may have an internal reference to the mock, creating MockContext for testing.
+        final Context c = new MockContext(mock(Context.class));
+
+        new ConnectivityManager(c, mService);
+
+        return new WeakReference<>(c);
+    }
+
+    private void forceGC() {
+        // First GC ensures that objects are collected for finalization, then second GC ensures
+        // they're garbage-collected after being finalized.
+        System.gc();
+        System.runFinalization();
+        System.gc();
+    }
+
+    @Test
+    public void testConnectivityManagerDoesNotLeakContext() throws Exception {
+        final WeakReference<Context> ref = makeConnectivityManagerAndReturnContext();
+
+        final int attempts = 100;
+        final long waitIntervalMs = 50;
+        for (int i = 0; i < attempts; i++) {
+            forceGC();
+            if (ref.get() == null) break;
+
+            Thread.sleep(waitIntervalMs);
+        }
+
+        assertNull("ConnectivityManager weak reference still not null after " + attempts
+                    + " attempts", ref.get());
+    }
 }
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
index 8559c20..5cb014f 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_V6;
+
 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_V6, 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_V6, CHILD_PARAMS);
+        final IkeTunnelConnectionParams tunnelParams2 =
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, 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/IpSecAlgorithmTest.java b/tests/unit/java/android/net/IpSecAlgorithmTest.java
index c473e82..1482055 100644
--- a/tests/unit/java/android/net/IpSecAlgorithmTest.java
+++ b/tests/unit/java/android/net/IpSecAlgorithmTest.java
@@ -47,7 +47,7 @@
 /** Unit tests for {@link IpSecAlgorithm}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecAlgorithmTest {
     private static final byte[] KEY_MATERIAL;
 
diff --git a/tests/unit/java/android/net/IpSecConfigTest.java b/tests/unit/java/android/net/IpSecConfigTest.java
index b87cb48..9f83036 100644
--- a/tests/unit/java/android/net/IpSecConfigTest.java
+++ b/tests/unit/java/android/net/IpSecConfigTest.java
@@ -36,7 +36,7 @@
 /** Unit tests for {@link IpSecConfig}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecConfigTest {
 
     @Test
diff --git a/tests/unit/java/android/net/IpSecManagerTest.java b/tests/unit/java/android/net/IpSecManagerTest.java
index cda8eb7..335f539 100644
--- a/tests/unit/java/android/net/IpSecManagerTest.java
+++ b/tests/unit/java/android/net/IpSecManagerTest.java
@@ -52,7 +52,7 @@
 /** Unit tests for {@link IpSecManager}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecManagerTest {
 
     private static final int TEST_UDP_ENCAP_PORT = 34567;
diff --git a/tests/unit/java/android/net/IpSecTransformTest.java b/tests/unit/java/android/net/IpSecTransformTest.java
index 81375f1..c1bd719 100644
--- a/tests/unit/java/android/net/IpSecTransformTest.java
+++ b/tests/unit/java/android/net/IpSecTransformTest.java
@@ -32,7 +32,7 @@
 /** Unit tests for {@link IpSecTransform}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecTransformTest {
 
     @Test
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 bf5568d..d84328c 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -47,7 +47,7 @@
 private const val TEST_SUBID2 = 2
 
 @RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class NetworkIdentityTest {
     private val mockContext = mock(Context::class.java)
 
diff --git a/tests/unit/java/android/net/NetworkStatsAccessTest.java b/tests/unit/java/android/net/NetworkStatsAccessTest.java
index 97a93ca..a74056b 100644
--- a/tests/unit/java/android/net/NetworkStatsAccessTest.java
+++ b/tests/unit/java/android/net/NetworkStatsAccessTest.java
@@ -19,6 +19,7 @@
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.when;
 
 import android.Manifest;
@@ -66,6 +67,10 @@
         when(mContext.getSystemServiceName(DevicePolicyManager.class))
                 .thenReturn(Context.DEVICE_POLICY_SERVICE);
         when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(mDpm);
+        if (mContext.getSystemService(DevicePolicyManager.class) == null) {
+            // Test is using mockito-extended
+            doCallRealMethod().when(mContext).getSystemService(DevicePolicyManager.class);
+        }
 
         setHasCarrierPrivileges(false);
         setIsDeviceOwner(false);
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index 32c106d..b518a61 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -37,6 +37,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
+import android.annotation.NonNull;
 import android.content.res.Resources;
 import android.net.NetworkStatsCollection.Key;
 import android.os.Process;
@@ -76,6 +77,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Tests for {@link NetworkStatsCollection}.
@@ -534,50 +536,84 @@
         assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
     }
 
+    private static void assertCollectionEntries(
+            @NonNull Map<Key, NetworkStatsHistory> expectedEntries,
+            @NonNull NetworkStatsCollection collection) {
+        final Map<Key, NetworkStatsHistory> actualEntries = collection.getEntries();
+        assertEquals(expectedEntries.size(), actualEntries.size());
+        for (Key expectedKey : expectedEntries.keySet()) {
+            final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+            final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
+            assertNotNull(actualHistory);
+            assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
+            actualEntries.remove(expectedKey);
+        }
+        assertEquals(0, actualEntries.size());
+    }
+
     @Test
-    public void testBuilder() {
-        final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
-        final NetworkStats.Entry entry = new NetworkStats.Entry();
-        final NetworkIdentitySet ident = new NetworkIdentitySet();
-        final Key key1 = new Key(ident, 0, 0, 0);
-        final Key key2 = new Key(ident, 1, 0, 0);
+    public void testRemoveHistoryBefore() {
+        final NetworkIdentity testIdent = new NetworkIdentity.Builder()
+                .setSubscriberId(TEST_IMSI).build();
+        final Key key1 = new Key(Set.of(testIdent), 0, 0, 0);
+        final Key key2 = new Key(Set.of(testIdent), 1, 0, 0);
         final long bucketDuration = 10;
 
+        // Prepare entries for testing, with different bucket start timestamps.
         final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40,
                 4, 50, 5, 60);
-        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 10, 3,
+        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(20, 10, 3,
                 41, 7, 1, 0);
+        final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(30, 10, 1,
+                21, 70, 4, 1);
 
         NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5)
                 .addEntry(entry1)
                 .addEntry(entry2)
                 .build();
-
-        NetworkStatsHistory history2 = new NetworkStatsHistory(10, 5);
-
-        NetworkStatsCollection actualCollection = new NetworkStatsCollection.Builder(bucketDuration)
+        NetworkStatsHistory history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .addEntry(entry3)
+                .build();
+        NetworkStatsCollection collection = new NetworkStatsCollection.Builder(bucketDuration)
                 .addEntry(key1, history1)
                 .addEntry(key2, history2)
                 .build();
 
-        // The builder will omit any entry with empty history. Thus, history2
-        // is not expected in the result collection.
+        // Verify nothing is removed if the cutoff time is equal to bucketStart.
+        collection.removeHistoryBefore(10);
+        final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
         expectedEntries.put(key1, history1);
+        expectedEntries.put(key2, history2);
+        assertCollectionEntries(expectedEntries, collection);
 
-        final Map<Key, NetworkStatsHistory> actualEntries = actualCollection.getEntries();
+        // Verify entry1 will be removed if its bucket start before to cutoff timestamp.
+        collection.removeHistoryBefore(11);
+        history1 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .build();
+        history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .addEntry(entry3)
+                .build();
+        final Map<Key, NetworkStatsHistory> cutoff1Entries1 = new ArrayMap<>();
+        cutoff1Entries1.put(key1, history1);
+        cutoff1Entries1.put(key2, history2);
+        assertCollectionEntries(cutoff1Entries1, collection);
 
-        assertEquals(expectedEntries.size(), actualEntries.size());
-        for (Key expectedKey : expectedEntries.keySet()) {
-            final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+        // Verify entry2 will be removed if its bucket start covers by cutoff timestamp.
+        collection.removeHistoryBefore(22);
+        history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry3)
+                .build();
+        final Map<Key, NetworkStatsHistory> cutoffEntries2 = new ArrayMap<>();
+        // History1 is not expected since the collection will omit empty entries.
+        cutoffEntries2.put(key2, history2);
+        assertCollectionEntries(cutoffEntries2, collection);
 
-            final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
-            assertNotNull(actualHistory);
-
-            assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
-
-            actualEntries.remove(expectedKey);
-        }
-        assertEquals(0, actualEntries.size());
+        // Verify all entries will be removed if cutoff timestamp covers all.
+        collection.removeHistoryBefore(Long.MAX_VALUE);
+        assertEquals(0, collection.getEntries().size());
     }
 
     /**
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index c170605..43e331b 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -56,12 +56,11 @@
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
-import java.util.List;
 import java.util.Random;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NetworkStatsHistoryTest {
     private static final String TAG = "NetworkStatsHistoryTest";
 
@@ -271,7 +270,7 @@
     }
 
     @Test
-    public void testRemove() throws Exception {
+    public void testRemoveStartingBefore() throws Exception {
         stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
 
         // record some data across 24 buckets
@@ -279,28 +278,28 @@
         assertEquals(24, stats.size());
 
         // try removing invalid data; should be no change
-        stats.removeBucketsBefore(0 - DAY_IN_MILLIS);
+        stats.removeBucketsStartingBefore(0 - DAY_IN_MILLIS);
         assertEquals(24, stats.size());
 
         // try removing far before buckets; should be no change
-        stats.removeBucketsBefore(TEST_START - YEAR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START - YEAR_IN_MILLIS);
         assertEquals(24, stats.size());
 
         // try removing just moments into first bucket; should be no change
-        // since that bucket contains data beyond the cutoff
-        stats.removeBucketsBefore(TEST_START + SECOND_IN_MILLIS);
+        // since that bucket doesn't contain data starts before the cutoff
+        stats.removeBucketsStartingBefore(TEST_START);
         assertEquals(24, stats.size());
 
         // try removing single bucket
-        stats.removeBucketsBefore(TEST_START + HOUR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START + HOUR_IN_MILLIS);
         assertEquals(23, stats.size());
 
         // try removing multiple buckets
-        stats.removeBucketsBefore(TEST_START + (4 * HOUR_IN_MILLIS));
+        stats.removeBucketsStartingBefore(TEST_START + (4 * HOUR_IN_MILLIS));
         assertEquals(20, stats.size());
 
         // try removing all buckets
-        stats.removeBucketsBefore(TEST_START + YEAR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START + YEAR_IN_MILLIS);
         assertEquals(0, stats.size());
     }
 
@@ -350,7 +349,7 @@
                         stats.recordData(start, end, entry);
                     } else {
                         // trim something
-                        stats.removeBucketsBefore(r.nextLong());
+                        stats.removeBucketsStartingBefore(r.nextLong());
                     }
                 }
                 assertConsistent(stats);
@@ -533,40 +532,6 @@
         assertEquals(512L + 4096L, stats.getTotalBytes());
     }
 
-    @Test
-    public void testBuilder() {
-        final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 30, 40,
-                4, 50, 5, 60);
-        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 15, 3,
-                41, 7, 1, 0);
-        final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(7, 301, 11,
-                14, 31, 2, 80);
-
-        final NetworkStatsHistory statsEmpty = new NetworkStatsHistory
-                .Builder(HOUR_IN_MILLIS, 10).build();
-        assertEquals(0, statsEmpty.getEntries().size());
-        assertEquals(HOUR_IN_MILLIS, statsEmpty.getBucketDuration());
-
-        NetworkStatsHistory statsSingle = new NetworkStatsHistory
-                .Builder(HOUR_IN_MILLIS, 8)
-                .addEntry(entry1)
-                .build();
-        assertEquals(1, statsSingle.getEntries().size());
-        assertEquals(HOUR_IN_MILLIS, statsSingle.getBucketDuration());
-        assertEquals(entry1, statsSingle.getEntries().get(0));
-
-        NetworkStatsHistory statsMultiple = new NetworkStatsHistory
-                .Builder(SECOND_IN_MILLIS, 0)
-                .addEntry(entry1).addEntry(entry2).addEntry(entry3)
-                .build();
-        final List<NetworkStatsHistory.Entry> entries = statsMultiple.getEntries();
-        assertEquals(3, entries.size());
-        assertEquals(SECOND_IN_MILLIS, statsMultiple.getBucketDuration());
-        assertEquals(entry1, entries.get(0));
-        assertEquals(entry2, entries.get(1));
-        assertEquals(entry3, entries.get(2));
-    }
-
     private static void assertIndexBeforeAfter(
             NetworkStatsHistory stats, int before, int after, long time) {
         assertEquals("unexpected before", before, stats.getIndexBefore(time));
diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java
index b0cc16c..6d79869 100644
--- a/tests/unit/java/android/net/NetworkStatsTest.java
+++ b/tests/unit/java/android/net/NetworkStatsTest.java
@@ -61,7 +61,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NetworkStatsTest {
 
     private static final String TEST_IFACE = "test0";
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 453612f..3e9662d 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -29,12 +29,8 @@
 import android.net.NetworkStats.METERED_NO
 import android.net.NetworkStats.METERED_YES
 import android.net.NetworkStats.ROAMING_ALL
-import android.net.NetworkTemplate.MATCH_BLUETOOTH
-import android.net.NetworkTemplate.MATCH_CARRIER
-import android.net.NetworkTemplate.MATCH_ETHERNET
 import android.net.NetworkTemplate.MATCH_MOBILE
 import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
-import android.net.NetworkTemplate.MATCH_PROXY
 import android.net.NetworkTemplate.MATCH_WIFI
 import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
 import android.net.NetworkTemplate.NETWORK_TYPE_ALL
@@ -52,11 +48,9 @@
 import android.net.wifi.WifiInfo
 import android.os.Build
 import android.telephony.TelephonyManager
-import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
 import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.SC_V2
 import com.android.testutils.assertParcelSane
 import org.junit.Before
 import org.junit.Test
@@ -65,7 +59,6 @@
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
@@ -77,7 +70,7 @@
 private const val TEST_WIFI_KEY2 = "wifiKey2"
 
 @RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class NetworkTemplateTest {
     private val mockContext = mock(Context::class.java)
     private val mockWifiInfo = mock(WifiInfo::class.java)
@@ -555,140 +548,4 @@
             it.assertMatches(identMobileImsi3)
         }
     }
-
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
-    @Test
-    fun testBuilderMatchRules() {
-        // Verify unknown match rules cannot construct templates.
-        listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(it).build()
-            }
-        }
-
-        // Verify hidden match rules cannot construct templates.
-        listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(it).build()
-            }
-        }
-
-        // Verify template which matches metered cellular and carrier networks with
-        // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
-        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
-            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
-                    .setMeteredness(METERED_YES).build().let {
-                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
-                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
-                                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                        assertEquals(expectedTemplate, it)
-                    }
-        }
-
-        // Verify carrier template cannot be created without IMSI.
-        assertFailsWith<IllegalArgumentException> {
-            NetworkTemplate.Builder(MATCH_CARRIER).build()
-        }
-
-        // Verify template which matches metered cellular networks,
-        // regardless of IMSI. See buildTemplateMobileWildcard.
-        NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf<String>(),
-                    METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches metered cellular networks and ratType.
-        // See NetworkTemplate#buildTemplateMobileWithRatType.
-        NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
-                .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
-                .build().let {
-                    val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
-                            arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
-                            ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
-                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                    assertEquals(expectedTemplate, it)
-                }
-
-        // Verify template which matches all wifi networks,
-        // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
-        NetworkTemplate.Builder(MATCH_WIFI).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf<String>(),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches wifi networks with the given Wifi Network Key.
-        // See buildTemplateWifi(wifiNetworkKey).
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches all wifi networks with the
-        // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
-        NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
-                .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
-                    val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
-                            arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
-                            METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                    assertEquals(expectedTemplate, it)
-                }
-
-        // Verify template which matches ethernet and bluetooth networks.
-        // See buildTemplateEthernet and buildTemplateBluetooth.
-        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
-            NetworkTemplate.Builder(matchRule).build().let {
-                val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
-                        null /*subscriberIds*/, arrayOf<String>(),
-                        METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                        OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-                assertEquals(expectedTemplate, it)
-            }
-        }
-    }
-
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
-    @Test
-    fun testBuilderWifiNetworkKeys() {
-        // Verify template builder which generates same template with the given different
-        // sequence keys.
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
-                setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
-            val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
-                    setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches non-wifi networks with the given key is invalid.
-        listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
-                Integer.MAX_VALUE).forEach { matchRule ->
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
-            }
-        }
-
-        // Verify template which matches wifi networks with the given null key is invalid.
-        assertFailsWith<IllegalArgumentException> {
-            NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
-        }
-
-        // Verify template which matches wifi wildcard with the given empty key set.
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
-                    arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-    }
 }
diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java
index 91f2cdd..6820b40 100644
--- a/tests/unit/java/android/net/QosSocketFilterTest.java
+++ b/tests/unit/java/android/net/QosSocketFilterTest.java
@@ -16,8 +16,17 @@
 
 package android.net;
 
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import android.os.Build;
 
@@ -29,6 +38,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 
@@ -36,14 +46,14 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class QosSocketFilterTest {
-
+    private static final int TEST_NET_ID = 1777;
+    private final Network mNetwork = new Network(TEST_NET_ID);
     @Test
     public void testPortExactMatch() {
         final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
         final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
         assertTrue(QosSocketFilter.matchesAddress(
                 new InetSocketAddress(addressA, 10), addressB, 10, 10));
-
     }
 
     @Test
@@ -77,5 +87,90 @@
         assertFalse(QosSocketFilter.matchesAddress(
                 new InetSocketAddress(addressA, 10), addressB, 10, 10));
     }
+
+    @Test
+    public void testAddressMatchWithAnyLocalAddresses() {
+        final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
+        final InetAddress addressB = InetAddresses.parseNumericAddress("0.0.0.0");
+        assertTrue(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressA, 10), addressB, 10, 10));
+        assertFalse(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressB, 10), addressA, 10, 10));
+    }
+
+    @Test
+    public void testProtocolMatch() throws Exception {
+        DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 0));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 10));
+        DatagramSocket socketV6 = new DatagramSocket(new InetSocketAddress("::1", 0));
+        socketV6.connect(new InetSocketAddress("::1", socketV6.getLocalPort() + 10));
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socketV6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        assertTrue(socketFilter.matchesProtocol(IPPROTO_UDP));
+        assertTrue(socketFilter6.matchesProtocol(IPPROTO_UDP));
+        assertFalse(socketFilter.matchesProtocol(IPPROTO_TCP));
+        assertFalse(socketFilter6.matchesProtocol(IPPROTO_TCP));
+        socket.close();
+        socketV6.close();
+    }
+
+    @Test
+    public void testValidate() throws Exception {
+        DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 0));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 7));
+        DatagramSocket socketV6 = new DatagramSocket(new InetSocketAddress("::1", 0));
+
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socketV6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter.validate());
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter6.validate());
+        socket.close();
+        socketV6.close();
+    }
+
+    @Test
+    public void testValidateUnbind() throws Exception {
+        DatagramSocket socket;
+        socket = new DatagramSocket(null);
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        assertEquals(EX_TYPE_FILTER_SOCKET_NOT_BOUND, socketFilter.validate());
+        socket.close();
+    }
+
+    @Test
+    public void testValidateLocalAddressChanged() throws Exception {
+        DatagramSocket socket = new DatagramSocket(null);
+        DatagramSocket socket6 = new DatagramSocket(null);
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socket6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        socket.bind(new InetSocketAddress("127.0.0.1", 0));
+        socket6.bind(new InetSocketAddress("::1", 0));
+        assertEquals(EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, socketFilter.validate());
+        assertEquals(EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, socketFilter6.validate());
+        socket.close();
+        socket6.close();
+    }
+
+    @Test
+    public void testValidateRemoteAddressChanged() throws Exception {
+        DatagramSocket socket;
+        socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 53137));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 11));
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter.validate());
+        socket.disconnect();
+        assertEquals(EX_TYPE_FILTER_SOCKET_NOT_CONNECTED, socketFilter.validate());
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 13));
+        assertEquals(EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED, socketFilter.validate());
+        socket.close();
+    }
 }
 
diff --git a/tests/unit/java/android/net/QosSocketInfoTest.java b/tests/unit/java/android/net/QosSocketInfoTest.java
new file mode 100644
index 0000000..749c182
--- /dev/null
+++ b/tests/unit/java/android/net/QosSocketInfoTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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 android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class QosSocketInfoTest {
+    @Mock
+    private Network mMockNetwork = mock(Network.class);
+
+    @Test
+    public void testConstructWithSock() throws Exception {
+        ServerSocket server = new ServerSocket();
+        ServerSocket server6 = new ServerSocket();
+
+        InetSocketAddress clientAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress clientAddr6 = new InetSocketAddress("::1", 0);
+        InetSocketAddress serverAddr6 = new InetSocketAddress("::1", 0);
+        server.bind(serverAddr);
+        server6.bind(serverAddr6);
+        Socket socket = new Socket(serverAddr.getAddress(), server.getLocalPort(),
+                clientAddr.getAddress(), clientAddr.getPort());
+        Socket socket6 = new Socket(serverAddr6.getAddress(), server6.getLocalPort(),
+                clientAddr6.getAddress(), clientAddr6.getPort());
+        QosSocketInfo sockInfo = new QosSocketInfo(mMockNetwork, socket);
+        QosSocketInfo sockInfo6 = new QosSocketInfo(mMockNetwork, socket6);
+        assertTrue(sockInfo.getLocalSocketAddress()
+                .equals(new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort())));
+        assertTrue(sockInfo.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket.getRemoteSocketAddress()));
+        assertEquals(SOCK_STREAM, sockInfo.getSocketType());
+        assertTrue(sockInfo6.getLocalSocketAddress()
+                .equals(new InetSocketAddress(socket6.getLocalAddress(), socket6.getLocalPort())));
+        assertTrue(sockInfo6.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket6.getRemoteSocketAddress()));
+        assertEquals(SOCK_STREAM, sockInfo6.getSocketType());
+        socket.close();
+        socket6.close();
+        server.close();
+        server6.close();
+    }
+
+    @Test
+    public void testConstructWithDatagramSock() throws Exception {
+        InetSocketAddress clientAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress clientAddr6 = new InetSocketAddress("::1", 0);
+        InetSocketAddress serverAddr6 = new InetSocketAddress("::1", 0);
+        DatagramSocket socket = new DatagramSocket(null);
+        socket.setReuseAddress(true);
+        socket.bind(clientAddr);
+        socket.connect(serverAddr);
+        DatagramSocket socket6 = new DatagramSocket(null);
+        socket6.setReuseAddress(true);
+        socket6.bind(clientAddr);
+        socket6.connect(serverAddr);
+        QosSocketInfo sockInfo = new QosSocketInfo(mMockNetwork, socket);
+        QosSocketInfo sockInfo6 = new QosSocketInfo(mMockNetwork, socket6);
+        assertTrue(sockInfo.getLocalSocketAddress()
+                .equals((InetSocketAddress) socket.getLocalSocketAddress()));
+        assertTrue(sockInfo.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket.getRemoteSocketAddress()));
+        assertEquals(SOCK_DGRAM, sockInfo.getSocketType());
+        assertTrue(sockInfo6.getLocalSocketAddress()
+                .equals((InetSocketAddress) socket6.getLocalSocketAddress()));
+        assertTrue(sockInfo6.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket6.getRemoteSocketAddress()));
+        assertEquals(SOCK_DGRAM, sockInfo6.getSocketType());
+        socket.close();
+    }
+}
diff --git a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
index 743d39e..aa5a246 100644
--- a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
+++ b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
@@ -61,14 +61,6 @@
         assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L)
     }
 
-    @Test
-    fun testMaybeReadLegacyUid() {
-        val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
-        NetworkStatsDataMigrationUtils.readLegacyUid(builder,
-                getInputStreamForResource(R.raw.netstats_uid_v4), false /* taggedData */)
-        assertValues(builder.build(), 223, 106245210L, 710722L, 1130647496L, 1103989L)
-    }
-
     private fun assertValues(
         collection: NetworkStatsCollection,
         expectedSize: Int,
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 30b8fcd..32274bc 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -51,7 +51,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdManagerTest {
 
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index e5e7ebc..64355ed 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -42,7 +42,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdServiceInfoTest {
 
     public final static InetAddress LOCALHOST;
@@ -125,6 +125,7 @@
         fullInfo.setPort(4242);
         fullInfo.setHost(LOCALHOST);
         fullInfo.setNetwork(new Network(123));
+        fullInfo.setInterfaceIndex(456);
         checkParcelable(fullInfo);
 
         NsdServiceInfo noHostInfo = new NsdServiceInfo();
@@ -175,6 +176,7 @@
         assertEquals(original.getHost(), result.getHost());
         assertTrue(original.getPort() == result.getPort());
         assertEquals(original.getNetwork(), result.getNetwork());
+        assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
 
         // Assert equality of attribute map.
         Map<String, byte[]> originalMap = original.getAttributes();
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index 943a559..0a6d2f2 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_V4;
+
 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_V4, 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/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 025b28c..0c5420d 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -51,6 +51,17 @@
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+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.PROFILE_NETWORK_PREFERENCE_DEFAULT;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
@@ -104,6 +115,10 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_2;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_4;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -136,6 +151,8 @@
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_PROFILE;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_VPN;
 import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
+import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
+import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
 import static com.android.testutils.ConcurrentUtils.await;
 import static com.android.testutils.ConcurrentUtils.durationOf;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -159,8 +176,8 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.AdditionalMatchers.aryEq;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -194,6 +211,7 @@
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
 import android.app.usage.NetworkStatsManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -337,10 +355,13 @@
 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.networkstack.apishim.api29.ConstantsShim;
 import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
 import com.android.server.ConnectivityService.NetworkRequestInfo;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
+import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.connectivity.Nat464Xlat;
@@ -463,6 +484,9 @@
     private static final int TEST_APP_ID_2 = 104;
     private static final int TEST_WORK_PROFILE_APP_UID_2 =
             UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID_2);
+    private static final int TEST_APP_ID_3 = 105;
+    private static final int TEST_APP_ID_4 = 106;
+    private static final int TEST_APP_ID_5 = 107;
 
     private static final String CLAT_PREFIX = "v4-";
     private static final String MOBILE_IFNAME = "test_rmnet_data0";
@@ -538,7 +562,9 @@
     @Mock NetworkPolicyManager mNetworkPolicyManager;
     @Mock VpnProfileStore mVpnProfileStore;
     @Mock SystemConfigManager mSystemConfigManager;
+    @Mock DevicePolicyManager mDevicePolicyManager;
     @Mock Resources mResources;
+    @Mock ClatCoordinator mClatCoordinator;
     @Mock PacProxyManager mPacProxyManager;
     @Mock BpfNetMaps mBpfNetMaps;
     @Mock CarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
@@ -641,7 +667,8 @@
         @Override
         public ComponentName startService(Intent service) {
             final String action = service.getAction();
-            if (!VpnConfig.SERVICE_INTERFACE.equals(action)) {
+            if (!VpnConfig.SERVICE_INTERFACE.equals(action)
+                    && !ConstantsShim.ACTION_VPN_MANAGER_EVENT.equals(action)) {
                 fail("Attempt to start unknown service, action=" + action);
             }
             return new ComponentName(service.getPackage(), "com.android.test.Service");
@@ -658,6 +685,7 @@
             if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
             if (Context.ETHERNET_SERVICE.equals(name)) return mEthernetManager;
             if (Context.NETWORK_POLICY_SERVICE.equals(name)) return mNetworkPolicyManager;
+            if (Context.DEVICE_POLICY_SERVICE.equals(name)) return mDevicePolicyManager;
             if (Context.SYSTEM_CONFIG_SERVICE.equals(name)) return mSystemConfigManager;
             if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager;
             if (Context.BATTERY_STATS_SERVICE.equals(name)) return mBatteryStatsManager;
@@ -686,6 +714,14 @@
             doReturn(value).when(mUserManager).isManagedProfile(eq(userHandle.getIdentifier()));
         }
 
+        public void setDeviceOwner(@NonNull final UserHandle userHandle, String value) {
+            // This relies on all contexts for a given user returning the same UM mock
+            final DevicePolicyManager dpmMock = createContextAsUser(userHandle, 0 /* flags */)
+                    .getSystemService(DevicePolicyManager.class);
+            doReturn(value).when(dpmMock).getDeviceOwner();
+            doReturn(value).when(mDevicePolicyManager).getDeviceOwner();
+        }
+
         @Override
         public ContentResolver getContentResolver() {
             return mContentResolver;
@@ -780,6 +816,32 @@
         }
     }
 
+    // This was only added in the T SDK, but this test needs to build against the R+S SDKs, too.
+    private static int toSdkSandboxUid(int appUid) {
+        final int firstSdkSandboxUid = 20000;
+        return appUid + (firstSdkSandboxUid - Process.FIRST_APPLICATION_UID);
+    }
+
+    // This function assumes the UID range for user 0 ([1, 99999])
+    private static UidRangeParcel[] uidRangeParcelsExcludingUids(Integer... excludedUids) {
+        int start = 1;
+        Arrays.sort(excludedUids);
+        List<UidRangeParcel> parcels = new ArrayList<UidRangeParcel>();
+        for (int excludedUid : excludedUids) {
+            if (excludedUid == start) {
+                start++;
+            } else {
+                parcels.add(new UidRangeParcel(start, excludedUid - 1));
+                start = excludedUid + 1;
+            }
+        }
+        if (start <= 99999) {
+            parcels.add(new UidRangeParcel(start, 99999));
+        }
+
+        return parcels.toArray(new UidRangeParcel[0]);
+    }
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
         waitForIdle(mCellNetworkAgent, TIMEOUT_MS);
@@ -1373,6 +1435,10 @@
             return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork();
         }
 
+        public NetworkAgentConfig getNetworkAgentConfig() {
+            return null == mMockNetworkAgent ? null : mMockNetworkAgent.getNetworkAgentConfig();
+        }
+
         @Override
         public int getActiveVpnType() {
             return mVpnType;
@@ -1999,6 +2065,11 @@
             return mBpfNetMaps;
         }
 
+        @Override
+        public ClatCoordinator getClatCoordinator(INetd netd) {
+            return mClatCoordinator;
+        }
+
         final ArrayTrackRecord<Pair<String, Long>> mRateLimitHistory = new ArrayTrackRecord<>();
         final Map<String, Long> mActiveRateLimit = new HashMap<>();
 
@@ -2936,6 +3007,7 @@
     @Test
     public void testRequiresValidation() {
         assertTrue(NetworkMonitorUtils.isValidationRequired(
+                NetworkAgentConfigShimImpl.newInstance(null),
                 mCm.getDefaultRequest().networkCapabilities));
     }
 
@@ -5845,7 +5917,7 @@
     }
 
     /**
-     * Validate the callback flow CBS request without carrier privilege.
+     * Validate the service throws if request with CBS but without carrier privilege.
      */
     @Test
     public void testCBSRequestWithoutCarrierPrivilege() throws Exception {
@@ -5854,10 +5926,8 @@
         final TestNetworkCallback networkCallback = new TestNetworkCallback();
 
         mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
-        // Now file the test request and expect it.
-        mCm.requestNetwork(nr, networkCallback);
-        networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null);
-        mCm.unregisterNetworkCallback(networkCallback);
+        // Now file the test request and expect the service throws.
+        assertThrows(SecurityException.class, () -> mCm.requestNetwork(nr, networkCallback));
     }
 
     private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
@@ -7933,6 +8003,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 */);
 
@@ -8083,6 +8154,7 @@
         assertTrue(nc.hasCapability(NET_CAPABILITY_INTERNET));
 
         assertFalse(NetworkMonitorUtils.isValidationRequired(
+                NetworkAgentConfigShimImpl.newInstance(mMockVpn.getNetworkAgentConfig()),
                 mMockVpn.getAgent().getNetworkCapabilities()));
         assertTrue(NetworkMonitorUtils.isPrivateDnsValidationRequired(
                 mMockVpn.getAgent().getNetworkCapabilities()));
@@ -8981,10 +9053,16 @@
                 allowList);
         waitForIdle();
 
-        UidRangeParcel firstHalf = new UidRangeParcel(1, VPN_UID - 1);
-        UidRangeParcel secondHalf = new UidRangeParcel(VPN_UID + 1, 99999);
+        final Set<Integer> excludedUids = new ArraySet<Integer>();
+        excludedUids.add(VPN_UID);
+        if (SdkLevel.isAtLeastT()) {
+            // On T onwards, the corresponding SDK sandbox UID should also be excluded
+            excludedUids.add(toSdkSandboxUid(VPN_UID));
+        }
+        final UidRangeParcel[] uidRangeParcels = uidRangeParcelsExcludingUids(
+                excludedUids.toArray(new Integer[0]));
         InOrder inOrder = inOrder(mMockNetd);
-        expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
 
         // Connect a network when lockdown is active, expect to see it blocked.
         mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
@@ -9008,7 +9086,7 @@
         vpnUidCallback.assertNoCallback();
         vpnUidDefaultCallback.assertNoCallback();
         vpnDefaultCallbackAsUid.assertNoCallback();
-        expectNetworkRejectNonSecureVpn(inOrder, false, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcels);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
         assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
@@ -9025,13 +9103,14 @@
         vpnUidDefaultCallback.assertNoCallback();
         vpnDefaultCallbackAsUid.assertNoCallback();
 
-        // The following requires that the UID of this test package is greater than VPN_UID. This
-        // is always true in practice because a plain AOSP build with no apps installed has almost
-        // 200 packages installed.
-        final UidRangeParcel piece1 = new UidRangeParcel(1, VPN_UID - 1);
-        final UidRangeParcel piece2 = new UidRangeParcel(VPN_UID + 1, uid - 1);
-        final UidRangeParcel piece3 = new UidRangeParcel(uid + 1, 99999);
-        expectNetworkRejectNonSecureVpn(inOrder, true, piece1, piece2, piece3);
+        excludedUids.add(uid);
+        if (SdkLevel.isAtLeastT()) {
+            // On T onwards, the corresponding SDK sandbox UID should also be excluded
+            excludedUids.add(toSdkSandboxUid(uid));
+        }
+        final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs = uidRangeParcelsExcludingUids(
+                excludedUids.toArray(new Integer[0]));
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcelsAlsoExcludingUs);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
         assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
@@ -9057,12 +9136,12 @@
         // Everything should now be blocked.
         mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
         waitForIdle();
-        expectNetworkRejectNonSecureVpn(inOrder, false, piece1, piece2, piece3);
+        expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcelsAlsoExcludingUs);
         allowList.clear();
         mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */,
                 allowList);
         waitForIdle();
-        expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
         defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent);
         assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent);
         vpnUidCallback.assertNoCallback();
@@ -9434,6 +9513,135 @@
         b2.expectBroadcast();
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testLockdownSetFirewallUidRule() throws Exception {
+        // For ConnectivityService#setAlwaysOnVpnPackage.
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
+        // Needed to call Vpn#setAlwaysOnPackage.
+        mServiceContext.setPermission(Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
+        // Needed to call Vpn#isAlwaysOnPackageSupported.
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        // Enable Lockdown
+        final ArrayList<String> allowList = new ArrayList<>();
+        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
+                true /* lockdown */, allowList);
+        waitForIdle();
+
+        // Lockdown rule is set to apps uids
+        verify(mBpfNetMaps).setUidRule(
+                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_DENY));
+        verify(mBpfNetMaps).setUidRule(
+                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_DENY));
+
+        reset(mBpfNetMaps);
+
+        // Disable lockdown
+        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
+                allowList);
+        waitForIdle();
+
+        // Lockdown rule is removed from apps uids
+        verify(mBpfNetMaps).setUidRule(
+                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_ALLOW));
+        verify(mBpfNetMaps).setUidRule(
+                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_ALLOW));
+
+        // Interface rules are not changed by Lockdown mode enable/disable
+        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
+    }
+
+    private void doTestSetUidFirewallRule(final int chain, final int defaultRule) {
+        final int uid = 1001;
+        mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_ALLOW);
+        verify(mBpfNetMaps).setUidRule(chain, uid, FIREWALL_RULE_ALLOW);
+        reset(mBpfNetMaps);
+
+        mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_DENY);
+        verify(mBpfNetMaps).setUidRule(chain, uid, FIREWALL_RULE_DENY);
+        reset(mBpfNetMaps);
+
+        mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_DEFAULT);
+        verify(mBpfNetMaps).setUidRule(chain, uid, defaultRule);
+        reset(mBpfNetMaps);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testSetUidFirewallRule() throws Exception {
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_POWERSAVE, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_1, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_2, FIREWALL_RULE_ALLOW);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testSetFirewallChainEnabled() throws Exception {
+        final List<Integer> firewallChains = Arrays.asList(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY,
+                FIREWALL_CHAIN_POWERSAVE,
+                FIREWALL_CHAIN_RESTRICTED,
+                FIREWALL_CHAIN_LOW_POWER_STANDBY,
+                FIREWALL_CHAIN_OEM_DENY_1,
+                FIREWALL_CHAIN_OEM_DENY_2);
+        for (final int chain: firewallChains) {
+            mCm.setFirewallChainEnabled(chain, true /* enabled */);
+            verify(mBpfNetMaps).setChildChain(chain, true /* enable */);
+            reset(mBpfNetMaps);
+
+            mCm.setFirewallChainEnabled(chain, false /* enabled */);
+            verify(mBpfNetMaps).setChildChain(chain, false /* enable */);
+            reset(mBpfNetMaps);
+        }
+    }
+
+    private void doTestReplaceFirewallChain(final int chain, final String chainName,
+            final boolean allowList) {
+        final int[] uids = new int[] {1001, 1002};
+        mCm.replaceFirewallChain(chain, uids);
+        verify(mBpfNetMaps).replaceUidChain(chainName, allowList, uids);
+        reset(mBpfNetMaps);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testReplaceFirewallChain() {
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_DOZABLE, "fw_dozable", true);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_STANDBY, "fw_standby", false);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_POWERSAVE, "fw_powersave",  true);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_RESTRICTED, "fw_restricted", true);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_LOW_POWER_STANDBY, "fw_low_power_standby", true);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_1, "fw_oem_deny_1", false);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_2, "fw_oem_deny_2", false);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testInvalidFirewallChain() throws Exception {
+        final int uid = 1001;
+        final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+        assertThrows(expected,
+                () -> mCm.setUidFirewallRule(-1 /* chain */, uid, FIREWALL_RULE_ALLOW));
+        assertThrows(expected,
+                () -> mCm.setUidFirewallRule(100 /* chain */, uid, FIREWALL_RULE_ALLOW));
+        assertThrows(expected, () -> mCm.replaceFirewallChain(-1 /* chain */, new int[]{uid}));
+        assertThrows(expected, () -> mCm.replaceFirewallChain(100 /* chain */, new int[]{uid}));
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testInvalidFirewallRule() throws Exception {
+        final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+        assertThrows(expected,
+                () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_DOZABLE,
+                        1001 /* uid */, -1 /* rule */));
+        assertThrows(expected,
+                () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_DOZABLE,
+                        1001 /* uid */, 100 /* rule */));
+    }
+
     /**
      * Test mutable and requestable network capabilities such as
      * {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
@@ -9561,6 +9769,59 @@
         return event;
     }
 
+    private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t);
+        } else {
+            return verify(t);
+        }
+    }
+
+    private <T> T verifyNeverWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t, never());
+        } else {
+            return verify(t, never());
+        }
+    }
+
+    private void verifyClatdStart(@Nullable InOrder inOrder, @NonNull String iface, int netId,
+            @NonNull String nat64Prefix) throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyWithOrder(inOrder, mClatCoordinator)
+                .clatStart(eq(iface), eq(netId), eq(new IpPrefix(nat64Prefix)));
+        } else {
+            verifyWithOrder(inOrder, mMockNetd).clatdStart(eq(iface), eq(nat64Prefix));
+        }
+    }
+
+    private void verifyNeverClatdStart(@Nullable InOrder inOrder, @NonNull String iface)
+            throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyNeverWithOrder(inOrder, mClatCoordinator).clatStart(eq(iface), anyInt(), any());
+        } else {
+            verifyNeverWithOrder(inOrder, mMockNetd).clatdStart(eq(iface), anyString());
+        }
+    }
+
+    private void verifyClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
+            throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyWithOrder(inOrder, mClatCoordinator).clatStop();
+        } else {
+            verifyWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
+        }
+    }
+
+    private void verifyNeverClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
+            throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyNeverWithOrder(inOrder, mClatCoordinator).clatStop();
+        } else {
+            verifyNeverWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
+        }
+    }
+
     @Test
     public void testStackedLinkProperties() throws Exception {
         final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -9593,6 +9854,7 @@
         mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
         reset(mMockDnsResolver);
         reset(mMockNetd);
+        reset(mClatCoordinator);
 
         // Connect with ipv6 link properties. Expect prefix discovery to be started.
         mCellNetworkAgent.connect(true);
@@ -9631,8 +9893,10 @@
                 && ri.iface != null && ri.iface.startsWith("v4-")));
 
         verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mClatCoordinator);
         verifyNoMoreInteractions(mMockDnsResolver);
         reset(mMockNetd);
+        reset(mClatCoordinator);
         reset(mMockDnsResolver);
         doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
                 .interfaceGetCfg(CLAT_MOBILE_IFNAME);
@@ -9653,7 +9917,7 @@
                 CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent).getLp();
         assertEquals(0, lpBeforeClat.getStackedLinks().size());
         assertEquals(kNat64Prefix, lpBeforeClat.getNat64Prefix());
-        verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+        verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
         clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9685,6 +9949,7 @@
                     new int[] { TRANSPORT_CELLULAR })));
         }
         reset(mMockNetd);
+        reset(mClatCoordinator);
         doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
                 .interfaceGetCfg(CLAT_MOBILE_IFNAME);
         // Change the NAT64 prefix without first removing it.
@@ -9693,11 +9958,12 @@
                 cellNetId, PREFIX_OPERATION_ADDED, kOtherNat64PrefixString, 96));
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getStackedLinks().size() == 0);
-        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
         assertRoutesRemoved(cellNetId, stackedDefault);
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
-        verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kOtherNat64Prefix.toString());
+        verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId,
+                kOtherNat64Prefix.toString());
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getNat64Prefix().equals(kOtherNat64Prefix));
         clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9706,6 +9972,7 @@
         assertRoutesAdded(cellNetId, stackedDefault);
         verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
         reset(mMockNetd);
+        reset(mClatCoordinator);
 
         // Add ipv4 address, expect that clatd and prefix discovery are stopped and stacked
         // linkproperties are cleaned up.
@@ -9714,7 +9981,7 @@
         mCellNetworkAgent.sendLinkProperties(cellLp);
         networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
         assertRoutesAdded(cellNetId, ipv4Subnet);
-        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
         verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId);
 
         // As soon as stop is called, the linkproperties lose the stacked interface.
@@ -9731,8 +9998,10 @@
         networkCallback.assertNoCallback();
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
         verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mClatCoordinator);
         verifyNoMoreInteractions(mMockDnsResolver);
         reset(mMockNetd);
+        reset(mClatCoordinator);
         reset(mMockDnsResolver);
         doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
                 .interfaceGetCfg(CLAT_MOBILE_IFNAME);
@@ -9754,7 +10023,7 @@
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent(
                 cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96));
         networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
-        verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+        verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
         clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9771,7 +10040,7 @@
         assertRoutesRemoved(cellNetId, ipv4Subnet, stackedDefault);
 
         // Stop has no effect because clat is already stopped.
-        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getStackedLinks().size() == 0);
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
@@ -9784,7 +10053,9 @@
                 eq(Integer.toString(TRANSPORT_CELLULAR)));
         verify(mMockNetd).networkDestroy(cellNetId);
         verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mClatCoordinator);
         reset(mMockNetd);
+        reset(mClatCoordinator);
 
         // Test disconnecting a network that is running 464xlat.
 
@@ -9801,7 +10072,7 @@
         assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default);
 
         // Clatd is started and clat iface comes up. Expect stacked link to be added.
-        verify(mMockNetd).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+        verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
         clat = getNat464Xlat(mCellNetworkAgent);
         clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
@@ -9811,16 +10082,18 @@
         // assertRoutesAdded sees all calls since last mMockNetd reset, so expect IPv6 routes again.
         assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default, stackedDefault);
         reset(mMockNetd);
+        reset(mClatCoordinator);
 
         // Disconnect the network. clat is stopped and the network is destroyed.
         mCellNetworkAgent.disconnect();
         networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
         networkCallback.assertNoCallback();
-        verify(mMockNetd).clatdStop(MOBILE_IFNAME);
+        verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
         verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
                 eq(Integer.toString(TRANSPORT_CELLULAR)));
         verify(mMockNetd).networkDestroy(cellNetId);
         verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mClatCoordinator);
 
         mCm.unregisterNetworkCallback(networkCallback);
     }
@@ -9851,7 +10124,7 @@
         baseLp.addDnsServer(InetAddress.getByName("2001:4860:4860::6464"));
 
         reset(mMockNetd, mMockDnsResolver);
-        InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver);
+        InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver, mClatCoordinator);
 
         // If a network already has a NAT64 prefix on connect, clatd is started immediately and
         // prefix discovery is never started.
@@ -9862,7 +10135,7 @@
         final Network network = mWiFiNetworkAgent.getNetwork();
         int netId = network.getNetId();
         callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         callback.assertNoCallback();
@@ -9872,7 +10145,7 @@
         lp.setNat64Prefix(null);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
         inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
 
@@ -9881,7 +10154,7 @@
         lp.setNat64Prefix(pref64FromRa);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa);
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
 
@@ -9890,22 +10163,22 @@
         lp.setNat64Prefix(null);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
         inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
 
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
                 makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96));
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns);
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromDns.toString());
 
         // If an RA advertises the same prefix that was discovered by DNS, nothing happens: prefix
         // discovery is not stopped, and there are no callbacks.
         lp.setNat64Prefix(pref64FromDns);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         callback.assertNoCallback();
-        inOrder.verify(mMockNetd, never()).clatdStop(iface);
-        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        verifyNeverClatdStop(inOrder, iface);
+        verifyNeverClatdStart(inOrder, iface);
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9914,8 +10187,8 @@
         lp.setNat64Prefix(null);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         callback.assertNoCallback();
-        inOrder.verify(mMockNetd, never()).clatdStop(iface);
-        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        verifyNeverClatdStop(inOrder, iface);
+        verifyNeverClatdStart(inOrder, iface);
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9924,14 +10197,14 @@
         lp.setNat64Prefix(pref64FromRa);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
 
         // Stopping prefix discovery results in a prefix removed notification.
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
                 makeNat64PrefixEvent(netId, PREFIX_OPERATION_REMOVED, pref64FromDnsStr, 96));
 
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
 
@@ -9939,9 +10212,9 @@
         lp.setNat64Prefix(newPref64FromRa);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, newPref64FromRa);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
-        inOrder.verify(mMockNetd).clatdStart(iface, newPref64FromRa.toString());
+        verifyClatdStart(inOrder, iface, netId, newPref64FromRa.toString());
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, newPref64FromRa.toString());
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
@@ -9951,8 +10224,8 @@
         mWiFiNetworkAgent.sendLinkProperties(lp);
         callback.assertNoCallback();
         assertEquals(newPref64FromRa, mCm.getLinkProperties(network).getNat64Prefix());
-        inOrder.verify(mMockNetd, never()).clatdStop(iface);
-        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        verifyNeverClatdStop(inOrder, iface);
+        verifyNeverClatdStart(inOrder, iface);
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9964,20 +10237,20 @@
         lp.setNat64Prefix(null);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
         inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
                 makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96));
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns);
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromDns.toString());
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), any());
 
         lp.setNat64Prefix(pref64FromDns);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         callback.assertNoCallback();
-        inOrder.verify(mMockNetd, never()).clatdStop(iface);
-        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        verifyNeverClatdStop(inOrder, iface);
+        verifyNeverClatdStart(inOrder, iface);
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9990,7 +10263,7 @@
         callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
         b.expectBroadcast();
 
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
 
@@ -10240,7 +10513,7 @@
         verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
         assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
         assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
-        assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange));
+        assertTrue(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0").equals(vpnRange));
 
         mMockVpn.disconnect();
         waitForIdle();
@@ -10248,11 +10521,11 @@
         // Disconnected VPN should have interface rules removed
         verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
         assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
-        assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0"));
+        assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
     }
 
     @Test
-    public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
+    public void testLegacyVpnInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
@@ -10262,13 +10535,34 @@
         mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
         assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
 
-        // Legacy VPN should not have interface rules set up
-        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        if (SdkLevel.isAtLeastT()) {
+            // On T and above, A connected Legacy VPN should have interface rules with null
+            // interface. Null Interface is a wildcard and this accepts traffic from all the
+            // interfaces. There are two expected invocations, one during the VPN initial
+            // connection, one during the VPN LinkProperties update.
+            ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+            verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+                    eq(null) /* iface */, uidCaptor.capture());
+            assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+            assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+                    vpnRange);
+
+            mMockVpn.disconnect();
+            waitForIdle();
+
+            // Disconnected VPN should have interface rules removed
+            verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+            assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
+        } else {
+            // Before T, Legacy VPN should not have interface rules.
+            verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        }
     }
 
     @Test
-    public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
-            throws Exception {
+    public void testLocalIpv4OnlyVpnInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
@@ -10278,8 +10572,31 @@
         mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
         assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
 
-        // IPv6 unreachable route should not be misinterpreted as a default route
-        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        if (SdkLevel.isAtLeastT()) {
+            // IPv6 unreachable route should not be misinterpreted as a default route
+            // On T and above, A connected VPN that does not provide a default route should have
+            // interface rules with null interface. Null Interface is a wildcard and this accepts
+            // traffic from all the interfaces. There are two expected invocations, one during the
+            // VPN initial connection, one during the VPN LinkProperties update.
+            ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+            verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+                    eq(null) /* iface */, uidCaptor.capture());
+            assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+            assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+                    vpnRange);
+
+            mMockVpn.disconnect();
+            waitForIdle();
+
+            // Disconnected VPN should have interface rules removed
+            verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+            assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
+        } else {
+            // Before T, VPN with IPv6 unreachable route should not have interface rules.
+            verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        }
     }
 
     @Test
@@ -11484,6 +11801,12 @@
         mCm.unregisterNetworkCallback(networkCallback);
     }
 
+    private void verifyDump(String[] args) {
+        final StringWriter stringWriter = new StringWriter();
+        mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), args);
+        assertFalse(stringWriter.toString().isEmpty());
+    }
+
     @Test
     public void testDumpDoesNotCrash() {
         mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
@@ -11496,11 +11819,26 @@
                 .addTransportType(TRANSPORT_WIFI).build();
         mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
         mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
-        final StringWriter stringWriter = new StringWriter();
 
-        mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]);
+        verifyDump(new String[0]);
 
-        assertFalse(stringWriter.toString().isEmpty());
+        // Verify dump with arguments.
+        final String dumpPrio = "--dump-priority";
+        final String[] dumpArgs = {dumpPrio};
+        verifyDump(dumpArgs);
+
+        final String[] highDumpArgs = {dumpPrio, "HIGH"};
+        verifyDump(highDumpArgs);
+
+        final String[] normalDumpArgs = {dumpPrio, "NORMAL"};
+        verifyDump(normalDumpArgs);
+
+        // Invalid args should do dumpNormal w/o exception
+        final String[] unknownDumpArgs = {dumpPrio, "UNKNOWN"};
+        verifyDump(unknownDumpArgs);
+
+        final String[] invalidDumpArgs = {"UNKNOWN"};
+        verifyDump(invalidDumpArgs);
     }
 
     @Test
@@ -11844,16 +12182,14 @@
         mQosCallbackMockHelper.registerQosCallback(
                 mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
 
-        final NetworkAgentWrapper.CallbackType.OnQosCallbackRegister cbRegister1 =
-                (NetworkAgentWrapper.CallbackType.OnQosCallbackRegister)
-                        wrapper.getCallbackHistory().poll(1000, x -> true);
+        final OnQosCallbackRegister cbRegister1 =
+                (OnQosCallbackRegister) wrapper.getCallbackHistory().poll(1000, x -> true);
         assertNotNull(cbRegister1);
 
         final int registerCallbackId = cbRegister1.mQosCallbackId;
         mService.unregisterQosCallback(mQosCallbackMockHelper.mCallback);
-        final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister;
-        cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister)
-                wrapper.getCallbackHistory().poll(1000, x -> true);
+        final OnQosCallbackUnregister cbUnregister =
+                (OnQosCallbackUnregister) wrapper.getCallbackHistory().poll(1000, x -> true);
         assertNotNull(cbUnregister);
         assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
         assertNull(wrapper.getCallbackHistory().poll(200, x -> true));
@@ -11932,6 +12268,86 @@
                         && session.getSessionType() == QosSession.TYPE_NR_BEARER));
     }
 
+    @Test @IgnoreUpTo(SC_V2)
+    public void testQosCallbackAvailableOnValidationError() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final NetworkAgentWrapper wrapper = mQosCallbackMockHelper.mAgentWrapper;
+        final int sessionId = 10;
+        final int qosCallbackId = 1;
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_NONE)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        OnQosCallbackRegister cbRegister1 =
+                (OnQosCallbackRegister) wrapper.getCallbackHistory().poll(1000, x -> true);
+        assertNotNull(cbRegister1);
+        final int registerCallbackId = cbRegister1.mQosCallbackId;
+
+        waitForIdle();
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+                1, 2, 3, 4, 5, new ArrayList<>());
+        mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+        waitForIdle();
+
+        final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister;
+        cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister)
+                wrapper.getCallbackHistory().poll(1000, x -> true);
+        assertNotNull(cbUnregister);
+        assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback)
+                .onError(eq(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED));
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testQosCallbackLostOnValidationError() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final int sessionId = 10;
+        final int qosCallbackId = 1;
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_NONE)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        waitForIdle();
+        EpsBearerQosSessionAttributes attributes =
+                sendQosSessionEvent(qosCallbackId, sessionId, true);
+        waitForIdle();
+
+        verify(mQosCallbackMockHelper.mCallback).onQosEpsBearerSessionAvailable(argThat(session ->
+                session.getSessionId() == sessionId
+                        && session.getSessionType() == QosSession.TYPE_EPS_BEARER), eq(attributes));
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+
+        sendQosSessionEvent(qosCallbackId, sessionId, false);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback)
+                .onError(eq(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED));
+    }
+
+    private EpsBearerQosSessionAttributes sendQosSessionEvent(
+            int qosCallbackId, int sessionId, boolean available) {
+        if (available) {
+            final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+                    1, 2, 3, 4, 5, new ArrayList<>());
+            mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                    .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+            return attributes;
+        } else {
+            mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                    .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_EPS_BEARER);
+            return null;
+        }
+
+    }
+
     @Test
     public void testQosCallbackTooManyRequests() throws Exception {
         mQosCallbackMockHelper = new QosCallbackMockHelper();
@@ -14455,7 +14871,7 @@
         profileNetworkPreferenceBuilder.setPreference(
                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
         profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(
-                NetworkCapabilities.NET_ENTERPRISE_ID_2);
+                NET_ENTERPRISE_ID_2);
         registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), true,
@@ -14480,6 +14896,393 @@
     }
 
     /**
+     * Make sure per-profile networking preference throws exception when default preference
+     * is set along with enterprise preference.
+     */
+    @Test
+    public void testPreferenceWithInvalidPreferenceDefaultAndEnterpriseTogether()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder1.build(),
+                                profileNetworkPreferenceBuilder2.build()),
+                        r -> r.run(), listener));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder2.build(),
+                                profileNetworkPreferenceBuilder1.build()),
+                        r -> r.run(), listener));
+    }
+
+    /**
+     * Make sure per profile network preferences behave as expected when two slices with
+     * two different apps within same user profile is configured
+     * Make sure per profile network preferences overrides with latest preference when
+     * same user preference is set twice
+     */
+    @Test
+    public void testSetPreferenceWithOverridingPreference()
+            throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+        registerDefaultNetworkCallbacks();
+
+        final TestNetworkCallback appCb1 = new TestNetworkCallback();
+        final TestNetworkCallback appCb2 = new TestNetworkCallback();
+        final TestNetworkCallback appCb3 = new TestNetworkCallback();
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        final int testWorkProfileAppUid2 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_2);
+        final int testWorkProfileAppUid3 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_3);
+
+        registerDefaultNetworkCallbackAsUid(appCb1, testWorkProfileAppUid1);
+        registerDefaultNetworkCallbackAsUid(appCb2, testWorkProfileAppUid2);
+        registerDefaultNetworkCallbackAsUid(appCb3, testWorkProfileAppUid3);
+
+        // Connect both a regular cell agent and an enterprise network first.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestNetworkAgentWrapper workAgent1 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_1);
+        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_2);
+        workAgent1.connect(true);
+        workAgent2.connect(true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        appCb1.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb2.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb3.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent1.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+        // Set preferences for testHandle to map testWorkProfileAppUid1 to
+        // NET_ENTERPRISE_ID_1 and testWorkProfileAppUid2 to NET_ENTERPRISE_ID_2.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_2);
+        profileNetworkPreferenceBuilder2.setIncludedUids(new int[]{testWorkProfileAppUid2});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder1.build(),
+                        profileNetworkPreferenceBuilder2.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb1.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(workAgent2);
+
+        // Set preferences for testHandle to map testWorkProfileAppUid3 to
+        // to NET_ENTERPRISE_ID_1.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder3 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder3.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder3.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder3.setIncludedUids(new int[]{testWorkProfileAppUid3});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder3.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb3.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb1.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        // Set the preferences for testHandle to default.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, appCb1, appCb2);
+        appCb3.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        workAgent2.disconnect();
+        mCellNetworkAgent.disconnect();
+
+        mCm.unregisterNetworkCallback(appCb1);
+        mCm.unregisterNetworkCallback(appCb2);
+        mCm.unregisterNetworkCallback(appCb3);
+        // Other callbacks will be unregistered by tearDown()
+    }
+
+    /**
+     * Make sure per profile network preferences behave as expected when multiple slices with
+     * multiple different apps within same user profile is configured.
+     */
+    @Test
+    public void testSetPreferenceWithMultiplePreferences()
+            throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+        registerDefaultNetworkCallbacks();
+
+        final TestNetworkCallback appCb1 = new TestNetworkCallback();
+        final TestNetworkCallback appCb2 = new TestNetworkCallback();
+        final TestNetworkCallback appCb3 = new TestNetworkCallback();
+        final TestNetworkCallback appCb4 = new TestNetworkCallback();
+        final TestNetworkCallback appCb5 = new TestNetworkCallback();
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        final int testWorkProfileAppUid2 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_2);
+        final int testWorkProfileAppUid3 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_3);
+        final int testWorkProfileAppUid4 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_4);
+        final int testWorkProfileAppUid5 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_5);
+
+        registerDefaultNetworkCallbackAsUid(appCb1, testWorkProfileAppUid1);
+        registerDefaultNetworkCallbackAsUid(appCb2, testWorkProfileAppUid2);
+        registerDefaultNetworkCallbackAsUid(appCb3, testWorkProfileAppUid3);
+        registerDefaultNetworkCallbackAsUid(appCb4, testWorkProfileAppUid4);
+        registerDefaultNetworkCallbackAsUid(appCb5, testWorkProfileAppUid5);
+
+        // Connect both a regular cell agent and an enterprise network first.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestNetworkAgentWrapper workAgent1 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_1);
+        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_2);
+        final TestNetworkAgentWrapper workAgent3 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_3);
+        final TestNetworkAgentWrapper workAgent4 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_4);
+        final TestNetworkAgentWrapper workAgent5 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_5);
+
+        workAgent1.connect(true);
+        workAgent2.connect(true);
+        workAgent3.connect(true);
+        workAgent4.connect(true);
+        workAgent5.connect(true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb1.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb2.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb3.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb4.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb5.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent1.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent3.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent4.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent5.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_2);
+        profileNetworkPreferenceBuilder2.setIncludedUids(new int[]{testWorkProfileAppUid2});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder3 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder3.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder3.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_3);
+        profileNetworkPreferenceBuilder3.setIncludedUids(new int[]{testWorkProfileAppUid3});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder4 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder4.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder4.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_4);
+        profileNetworkPreferenceBuilder4.setIncludedUids(new int[]{testWorkProfileAppUid4});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder5 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder5.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder5.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_5);
+        profileNetworkPreferenceBuilder5.setIncludedUids(new int[]{testWorkProfileAppUid5});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder1.build(),
+                        profileNetworkPreferenceBuilder2.build(),
+                        profileNetworkPreferenceBuilder3.build(),
+                        profileNetworkPreferenceBuilder4.build(),
+                        profileNetworkPreferenceBuilder5.build()),
+                r -> r.run(), listener);
+
+        listener.expectOnComplete();
+
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent3.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent4.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder4.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent5.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder5.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb1.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(workAgent2);
+        appCb3.expectAvailableCallbacksValidated(workAgent3);
+        appCb4.expectAvailableCallbacksValidated(workAgent4);
+        appCb5.expectAvailableCallbacksValidated(workAgent5);
+
+        workAgent1.disconnect();
+        workAgent2.disconnect();
+        workAgent3.disconnect();
+        workAgent4.disconnect();
+        workAgent5.disconnect();
+
+        appCb1.expectCallback(CallbackEntry.LOST, workAgent1);
+        appCb2.expectCallback(CallbackEntry.LOST, workAgent2);
+        appCb3.expectCallback(CallbackEntry.LOST, workAgent3);
+        appCb4.expectCallback(CallbackEntry.LOST, workAgent4);
+        appCb5.expectCallback(CallbackEntry.LOST, workAgent5);
+
+        appCb1.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb2.assertNoCallback();
+        appCb3.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb4.assertNoCallback();
+        appCb5.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd, never()).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd, never()).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder4.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder5.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        mSystemDefaultNetworkCallback.assertNoCallback();
+        mDefaultNetworkCallback.assertNoCallback();
+
+        // Set the preferences for testHandle to default.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, appCb1, appCb3,
+                appCb5);
+        appCb2.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb4.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+
+        mCm.unregisterNetworkCallback(appCb1);
+        mCm.unregisterNetworkCallback(appCb2);
+        mCm.unregisterNetworkCallback(appCb3);
+        mCm.unregisterNetworkCallback(appCb4);
+        mCm.unregisterNetworkCallback(appCb5);
+        // Other callbacks will be unregistered by tearDown()
+    }
+
+    /**
      * Test that, in a given networking context, calling setPreferenceForUser to set per-profile
      * defaults on then off works as expected.
      */
@@ -14649,12 +15452,42 @@
     public void testProfileNetworkPrefWrongProfile() throws Exception {
         final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
         mServiceContext.setWorkProfile(testHandle, false);
-        assertThrows("Should not be able to set a user pref for a non-work profile",
+        mServiceContext.setDeviceOwner(testHandle, null);
+        assertThrows("Should not be able to set a user pref for a non-work profile "
+                + "and non device owner",
                 IllegalArgumentException.class , () ->
                         mCm.setProfileNetworkPreference(testHandle,
                                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE, null, null));
     }
 
+    /**
+     * Make sure requests for per-profile default networking for a device owner is
+     * accepted on T and not accepted on S
+     */
+    @Test
+    public void testProfileNetworkDeviceOwner() throws Exception {
+        final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
+        mServiceContext.setWorkProfile(testHandle, false);
+        mServiceContext.setDeviceOwner(testHandle, "deviceOwnerPackage");
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        if (SdkLevel.isAtLeastT()) {
+            mCm.setProfileNetworkPreferences(testHandle,
+                    List.of(profileNetworkPreferenceBuilder.build()),
+                    r -> r.run(), listener);
+        } else {
+            // S should not allow setting preference on device owner
+            assertThrows("Should not be able to set a user pref for a non-work profile on S",
+                    IllegalArgumentException.class , () ->
+                            mCm.setProfileNetworkPreferences(testHandle,
+                                    List.of(profileNetworkPreferenceBuilder.build()),
+                                    r -> r.run(), listener));
+        }
+    }
+
     @Test
     public void testSubIdsClearedWithoutNetworkFactoryPermission() throws Exception {
         mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
@@ -14814,6 +15647,27 @@
     }
 
     @Test
+    public void testAutomotiveEthernetAllowedUids() throws Exception {
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+        mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
+
+        // In this test the automotive feature will be enabled.
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+
+        // Simulate a restricted ethernet network.
+        final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_ETHERNET)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET,
+                new LinkProperties(), agentNetCaps.build());
+        validateAllowedUids(mEthernetNetworkAgent, TRANSPORT_ETHERNET, agentNetCaps, true);
+    }
+
+    @Test
     public void testCbsAllowedUids() throws Exception {
         mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
         mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
@@ -14822,6 +15676,24 @@
         doReturn(true).when(mCarrierPrivilegeAuthenticator)
                 .hasCarrierPrivilegeForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
 
+        // Simulate a restricted telephony network. The telephony factory is entitled to set
+        // the access UID to the service package on any of its restricted networks.
+        final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
+                new LinkProperties(), agentNetCaps.build());
+        validateAllowedUids(mCellNetworkAgent, TRANSPORT_CELLULAR, agentNetCaps, false);
+    }
+
+    private void validateAllowedUids(final TestNetworkAgentWrapper testAgent,
+            @NetworkCapabilities.Transport final int transportUnderTest,
+            final NetworkCapabilities.Builder ncb, final boolean forAutomotive) throws Exception {
         final ArraySet<Integer> serviceUidSet = new ArraySet<>();
         serviceUidSet.add(TEST_PACKAGE_UID);
         final ArraySet<Integer> nonServiceUidSet = new ArraySet<>();
@@ -14832,40 +15704,34 @@
 
         final TestNetworkCallback cb = new TestNetworkCallback();
 
-        // Simulate a restricted telephony network. The telephony factory is entitled to set
-        // the access UID to the service package on any of its restricted networks.
-        final NetworkCapabilities.Builder ncb = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .addCapability(NET_CAPABILITY_INTERNET)
-                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
-
+        /* Test setting UIDs */
         // Cell gets to set the service UID as access UID
         mCm.requestNetwork(new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
+                .addTransportType(transportUnderTest)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .build(), cb);
-        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
-                new LinkProperties(), ncb.build());
-        mCellNetworkAgent.connect(true);
-        cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        testAgent.connect(true);
+        cb.expectAvailableThenValidatedCallbacks(testAgent);
         ncb.setAllowedUids(serviceUidSet);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(mCellNetworkAgent,
+            cb.expectCapabilitiesThat(testAgent,
                     caps -> caps.getAllowedUids().equals(serviceUidSet));
         } else {
             // S must ignore access UIDs.
             cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         }
 
+        /* Test setting UIDs is rejected when expected */
+        if (forAutomotive) {
+            mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false);
+        }
+
         // ...but not to some other UID. Rejection sets UIDs to the empty set
         ncb.setAllowedUids(nonServiceUidSet);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(mCellNetworkAgent,
+            cb.expectCapabilitiesThat(testAgent,
                     caps -> caps.getAllowedUids().isEmpty());
         } else {
             // S must ignore access UIDs.
@@ -14874,18 +15740,18 @@
 
         // ...and also not to multiple UIDs even including the service UID
         ncb.setAllowedUids(serviceUidSetPlus);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
 
-        mCellNetworkAgent.disconnect();
-        cb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        testAgent.disconnect();
+        cb.expectCallback(CallbackEntry.LOST, testAgent);
         mCm.unregisterNetworkCallback(cb);
 
         // Must be unset before touching the transports, because remove and add transport types
         // check the specifier on the builder immediately, contradicting normal builder semantics
         // TODO : fix the builder
         ncb.setNetworkSpecifier(null);
-        ncb.removeTransportType(TRANSPORT_CELLULAR);
+        ncb.removeTransportType(transportUnderTest);
         ncb.addTransportType(TRANSPORT_WIFI);
         // Wifi does not get to set access UID, even to the correct UID
         mCm.requestNetwork(new NetworkRequest.Builder()
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 45f3d3c..9401d47 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -89,7 +89,7 @@
 public class IpSecServiceParameterizedTest {
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
-            Build.VERSION_CODES.R /* ignoreClassUpTo */);
+            Build.VERSION_CODES.S_V2 /* ignoreClassUpTo */);
 
     private static final int TEST_SPI = 0xD1201D;
 
@@ -783,6 +783,23 @@
     }
 
     @Test
+    public void testSetNetworkForTunnelInterfaceFailsForNullLp() throws Exception {
+        final IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+        final Network newFakeNetwork = new Network(1000);
+        final int tunnelIfaceResourceId = createTunnelResp.resourceId;
+
+        try {
+            mIpSecService.setNetworkForTunnelInterface(
+                    tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE);
+            fail(
+                    "Expected an IllegalArgumentException for underlying network with null"
+                            + " LinkProperties");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
     public void testSetNetworkForTunnelInterfaceFailsForInvalidResourceId() throws Exception {
         final IpSecTunnelInterfaceResponse createTunnelResp =
                 createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
diff --git a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
index 5c7ca6f..8595ab9 100644
--- a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
@@ -54,7 +54,7 @@
 /** Unit tests for {@link IpSecService.RefcountedResource}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecServiceRefcountedResourceTest {
     Context mMockContext;
     IpSecService.Dependencies mMockDeps;
diff --git a/tests/unit/java/com/android/server/IpSecServiceTest.java b/tests/unit/java/com/android/server/IpSecServiceTest.java
index 7e6b157..6955620 100644
--- a/tests/unit/java/com/android/server/IpSecServiceTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceTest.java
@@ -75,7 +75,7 @@
 /** Unit tests for {@link IpSecService}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecServiceTest {
 
     private static final int DROID_SPI = 0xD1201D;
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 5086943..9365bee 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -21,8 +21,13 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doCallRealMethod;
 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;
@@ -34,8 +39,15 @@
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.ResolutionInfo;
 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 +61,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;
@@ -66,7 +75,6 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
 
 import java.util.LinkedList;
 import java.util.Queue;
@@ -76,7 +84,7 @@
 //  - test NSD_ON ENABLE/DISABLED listening
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdServiceTest {
 
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
@@ -92,8 +100,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 +119,21 @@
         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);
+        if (mContext.getSystemService(MDnsManager.class) == null) {
+            // Test is using mockito-extended
+            doCallRealMethod().when(mContext).getSystemService(MDnsManager.class);
+        }
+        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 +154,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 +180,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 +222,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 +232,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 +270,122 @@
         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();
+    }
+
+    @Test
+    public void testDiscoverOnTetheringDownstream() throws Exception {
+        NsdService service = makeService();
+        NsdManager client = connectClient(service);
+
+        final String serviceType = "a_type";
+        final String serviceName = "a_name";
+        final String domainName = "mytestdevice.local";
+        final int interfaceIdx = 123;
+        final NsdManager.DiscoveryListener discListener = mock(NsdManager.DiscoveryListener.class);
+        client.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discListener);
+        waitForIdle();
+
+        final ArgumentCaptor<IMDnsEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(IMDnsEventListener.class);
+        verify(mMockMDnsM).registerEventListener(listenerCaptor.capture());
+        final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(serviceType),
+                eq(0) /* interfaceIdx */);
+        // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
+        // this needs to use a timeout
+        verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(serviceType);
+
+        final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
+                discIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_FOUND,
+                serviceName,
+                serviceType,
+                domainName,
+                interfaceIdx,
+                INetd.LOCAL_NET_ID); // LOCAL_NET_ID (99) used on tethering downstreams
+        final IMDnsEventListener eventListener = listenerCaptor.getValue();
+        eventListener.onServiceDiscoveryStatus(discoveryInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<NsdServiceInfo> discoveredInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(discoveredInfoCaptor.capture());
+        final NsdServiceInfo foundInfo = discoveredInfoCaptor.getValue();
+        assertEquals(serviceName, foundInfo.getServiceName());
+        assertEquals(serviceType, foundInfo.getServiceType());
+        assertNull(foundInfo.getHost());
+        assertNull(foundInfo.getNetwork());
+        assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
+
+        // After discovering the service, verify resolving it
+        final NsdManager.ResolveListener resolveListener = mock(NsdManager.ResolveListener.class);
+        client.resolveService(foundInfo, resolveListener);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(serviceName), eq(serviceType),
+                eq("local.") /* domain */, eq(interfaceIdx));
+
+        final int servicePort = 10123;
+        final String serviceFullName = serviceName + "." + serviceType;
+        final ResolutionInfo resolutionInfo = new ResolutionInfo(
+                resolvIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_RESOLVED,
+                null /* serviceName */,
+                null /* serviceType */,
+                null /* domain */,
+                serviceFullName,
+                domainName,
+                servicePort,
+                new byte[0] /* txtRecord */,
+                interfaceIdx);
+
+        doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+        eventListener.onServiceResolutionStatus(resolutionInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(domainName),
+                eq(interfaceIdx));
+
+        final String serviceAddress = "192.0.2.123";
+        final GetAddressInfo addressInfo = new GetAddressInfo(
+                getAddrIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+                serviceFullName,
+                serviceAddress,
+                interfaceIdx,
+                INetd.LOCAL_NET_ID);
+        eventListener.onGettingServiceAddressStatus(addressInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<NsdServiceInfo> resInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(resInfoCaptor.capture());
+        final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
+        assertEquals(serviceName, resolvedService.getServiceName());
+        assertEquals("." + serviceType, resolvedService.getServiceType());
+        assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
+        assertEquals(servicePort, resolvedService.getPort());
+        assertNull(resolvedService.getNetwork());
+        assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
     }
 
     private void waitForIdle() {
@@ -267,11 +393,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 +407,6 @@
                 return super.connect(cb);
             }
         };
-        verify(mDaemon, never()).execute(any(String.class));
         return service;
     }
 
@@ -297,34 +418,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 8a2cfc2..3047a16 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -17,30 +17,47 @@
 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.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 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.spy;
+import static org.mockito.Mockito.verify;
 
 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.internal.util.IndentingPrintWriter;
+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;
+import com.android.testutils.TestBpfMap;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,6 +69,9 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.io.StringWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.util.Objects;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -61,9 +81,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;
 
@@ -74,18 +97,40 @@
 
     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(
-            new FileDescriptor());
-    private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
-            new FileDescriptor());
+    private static final ParcelFileDescriptor TUN_PFD = spy(new ParcelFileDescriptor(
+            new FileDescriptor()));
+    private static final ParcelFileDescriptor RAW_SOCK_PFD = spy(new ParcelFileDescriptor(
+            new FileDescriptor()));
+    private static final ParcelFileDescriptor PACKET_SOCK_PFD = spy(new ParcelFileDescriptor(
+            new FileDescriptor()));
+
+    private static final String EGRESS_PROG_PATH =
+            "/sys/fs/bpf/net_shared/prog_clatd_schedcls_egress4_clat_rawip";
+    private static final String INGRESS_PROG_PATH =
+            "/sys/fs/bpf/net_shared/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);
+
+    private final TestBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap =
+            spy(new TestBpfMap<>(ClatIngress6Key.class, ClatIngress6Value.class));
+    private final TestBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap =
+            spy(new TestBpfMap<>(ClatEgress4Key.class, ClatEgress4Value.class));
 
     @Mock private INetd mNetd;
     @Spy private TestDependencies mDeps = new TestDependencies();
@@ -129,6 +174,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;
@@ -285,6 +332,49 @@
                 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
@@ -309,12 +399,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),
@@ -327,6 +422,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),
@@ -360,6 +456,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.
@@ -369,9 +472,16 @@
 
         // [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.
@@ -404,4 +514,190 @@
         // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
         assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
     }
+
+    @Test
+    public void testDump() throws Exception {
+        final ClatCoordinator coordinator = makeClatCoordinator();
+        final StringWriter stringWriter = new StringWriter();
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ");
+        coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+        coordinator.dump(ipw);
+
+        final String[] dumpStrings = stringWriter.toString().split("\n");
+        assertEquals(5, dumpStrings.length);
+        assertEquals("Forwarding rules:", dumpStrings[0].trim());
+        assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif",
+                dumpStrings[1].trim());
+        assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
+                dumpStrings[2].trim());
+        assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
+                dumpStrings[3].trim());
+        assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
+                dumpStrings[4].trim());
+    }
+
+    @Test
+    public void testNotStartClatWithInvalidPrefix() throws Exception {
+        final ClatCoordinator coordinator = makeClatCoordinator();
+        final IpPrefix invalidPrefix = new IpPrefix("2001:db8::/64");
+        assertThrows(IOException.class,
+                () -> coordinator.clatStart(BASE_IFACE, NETID, invalidPrefix));
+    }
+
+    private void assertStartClat(final TestDependencies deps) throws Exception {
+        final ClatCoordinator coordinator = new ClatCoordinator(deps);
+        assertNotNull(coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+    }
+
+    private void assertNotStartClat(final TestDependencies deps) {
+        // Expect that the injection function of TestDependencies causes clatStart() failed.
+        final ClatCoordinator coordinator = new ClatCoordinator(deps);
+        assertThrows(IOException.class,
+                () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+    }
+
+    private void checkNotStartClat(final TestDependencies deps, final boolean verifyTunFd,
+            final boolean verifyPacketSockFd, final boolean verifyRawSockFd) throws Exception {
+        // [1] Expect that modified TestDependencies can't start clatd.
+        clearInvocations(TUN_PFD, RAW_SOCK_PFD, PACKET_SOCK_PFD);
+        assertNotStartClat(deps);
+        if (verifyTunFd) verify(TUN_PFD).close();
+        if (verifyPacketSockFd) verify(PACKET_SOCK_PFD).close();
+        if (verifyRawSockFd) verify(RAW_SOCK_PFD).close();
+
+        // [2] Expect that unmodified TestDependencies can start clatd.
+        // Used to make sure that the above modified TestDependencies has really broken the
+        // clatd starting.
+        assertStartClat(new TestDependencies());
+    }
+
+    // The following testNotStartClat* tests verifies bunches of code for unwinding the
+    // failure if any.
+    @Test
+    public void testNotStartClatWithNativeFailureSelectIpv4Address() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+                    throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), false /* verifyTunFd */,
+                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureGenerateIpv6Address() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+                    @NonNull String prefix64) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), false /* verifyTunFd */,
+                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureCreateTunInterface() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int createTunInterface(@NonNull String tuniface) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), false /* verifyTunFd */,
+                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureDetectMtu() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+                    throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
+                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureOpenPacketSocket() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int openPacketSocket() throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
+                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureOpenRawSocket6() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int openRawSocket6(int mark) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
+                true /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureAddAnycastSetsockopt() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6,
+                    int ifindex) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
+                true /* verifyPacketSockFd */, true /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureTagSocketAsClat() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public long tagSocketAsClat(@NonNull FileDescriptor sock) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
+                true /* verifyPacketSockFd */, true /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureConfigurePacketSocket() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public void configurePacketSocket(@NonNull FileDescriptor sock, String v6,
+                    int ifindex) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
+                true /* verifyPacketSockFd */, true /* verifyRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureStartClatd() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+                    @NonNull FileDescriptor writesock6, @NonNull String iface,
+                    @NonNull String pfx96, @NonNull String v4, @NonNull String v6)
+                    throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
+                true /* verifyPacketSockFd */, true /* verifyRawSockFd */);
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index e7f6245..c03a9cd 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -22,6 +22,7 @@
 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
@@ -32,11 +33,12 @@
 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
 
@@ -63,6 +65,23 @@
         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
     fun testGetLegacyInt() {
         val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE)
@@ -101,10 +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 ->
diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
index aa4c4e3..06e0d6d 100644
--- a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -21,6 +21,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
@@ -44,8 +46,11 @@
 import android.os.Handler;
 import android.os.test.TestLooper;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.server.ConnectivityService;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -75,13 +80,20 @@
     @Mock IDnsResolver mDnsResolver;
     @Mock INetd mNetd;
     @Mock NetworkAgentInfo mNai;
+    @Mock ClatCoordinator mClatCoordinator;
 
     TestLooper mLooper;
     Handler mHandler;
     NetworkAgentConfig mAgentConfig = new NetworkAgentConfig();
 
     Nat464Xlat makeNat464Xlat(boolean isCellular464XlatEnabled) {
-        return new Nat464Xlat(mNai, mNetd, mDnsResolver, new ConnectivityService.Dependencies()) {
+        final ConnectivityService.Dependencies deps = new ConnectivityService.Dependencies() {
+            @Override public ClatCoordinator getClatCoordinator(INetd netd) {
+                return mClatCoordinator;
+            }
+        };
+
+        return new Nat464Xlat(mNai, mNetd, mDnsResolver, deps) {
             @Override protected int getNetId() {
                 return NETID;
             }
@@ -208,6 +220,39 @@
         }
     }
 
+    private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t);
+        } else {
+            return verify(t);
+        }
+    }
+
+    private void verifyClatdStart(@Nullable InOrder inOrder) throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyWithOrder(inOrder, mClatCoordinator)
+                .clatStart(eq(BASE_IFACE), eq(NETID), eq(new IpPrefix(NAT64_PREFIX)));
+        } else {
+            verifyWithOrder(inOrder, mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        }
+    }
+
+    private void verifyNeverClatdStart() throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verify(mClatCoordinator, never()).clatStart(anyString(), anyInt(), any());
+        } else {
+            verify(mNetd, never()).clatdStart(anyString(), anyString());
+        }
+    }
+
+    private void verifyClatdStop(@Nullable InOrder inOrder) throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyWithOrder(inOrder, mClatCoordinator).clatStop();
+        } else {
+            verifyWithOrder(inOrder, mNetd).clatdStop(eq(BASE_IFACE));
+        }
+    }
+
     private void checkNormalStartAndStop(boolean dueToDisconnect) throws Exception {
         Nat464Xlat nat = makeNat464Xlat(true);
         ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
@@ -219,7 +264,7 @@
         // Start clat.
         nat.start();
 
-        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(null /* inOrder */);
 
         // Stacked interface up notification arrives.
         nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -235,7 +280,7 @@
         makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
-        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(null /* inOrder */);
         verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertTrue(c.getValue().getStackedLinks().isEmpty());
         assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
@@ -262,7 +307,7 @@
     private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception {
         Nat464Xlat nat = makeNat464Xlat(true);
         ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
-        InOrder inOrder = inOrder(mNetd, mConnectivity);
+        InOrder inOrder = inOrder(mNetd, mConnectivity, mClatCoordinator);
 
         mNai.linkProperties.addLinkAddress(V6ADDR);
 
@@ -270,7 +315,7 @@
 
         nat.start();
 
-        inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(inOrder);
 
         // Stacked interface up notification arrives.
         nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -284,7 +329,7 @@
         // ConnectivityService stops clat (Network disconnects, IPv4 addr appears, ...).
         nat.stop();
 
-        inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(inOrder);
 
         inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -306,7 +351,7 @@
 
         nat.start();
 
-        inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(inOrder);
 
         if (!interfaceRemovedFirst) {
             // Stacked interface removed notification arrives and is ignored.
@@ -328,7 +373,7 @@
         // ConnectivityService stops clat again.
         nat.stop();
 
-        inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(inOrder);
 
         inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -357,7 +402,7 @@
 
         nat.start();
 
-        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(null /* inOrder */);
 
         // Stacked interface up notification arrives.
         nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -373,7 +418,7 @@
         nat.interfaceRemoved(STACKED_IFACE);
         mLooper.dispatchNext();
 
-        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(null /* inOrder */);
         verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
         verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
         assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -395,13 +440,13 @@
 
         nat.start();
 
-        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(null /* inOrder */);
 
         // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
         makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
-        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(null /* inOrder */);
         verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
         assertIdle(nat);
 
@@ -437,13 +482,13 @@
 
         nat.start();
 
-        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(null /* inOrder */);
 
         // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
         makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
-        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(null /* inOrder */);
         verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
         assertIdle(nat);
 
@@ -518,7 +563,7 @@
             mNai.linkProperties.setNat64Prefix(nat64Prefix);
             nat.setNat64PrefixFromRa(nat64Prefix);
             nat.update();
-            verify(mNetd, never()).clatdStart(anyString(), anyString());
+            verifyNeverClatdStart();
             assertIdle(nat);
         } else {
             // Prefix discovery is started.
@@ -529,7 +574,7 @@
             mNai.linkProperties.setNat64Prefix(nat64Prefix);
             nat.setNat64PrefixFromRa(nat64Prefix);
             nat.update();
-            verify(mNetd).clatdStart(BASE_IFACE, NAT64_PREFIX);
+            verifyClatdStart(null /* inOrder */);
             assertStarting(nat);
         }
     }
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 6b379e8..ecd17ba 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -30,6 +30,9 @@
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -117,23 +120,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";
@@ -191,6 +203,7 @@
         mBpfMapMonitor = new BpfMapMonitor(mBpfNetMaps);
 
         doReturn(List.of()).when(mPackageManager).getInstalledPackagesAsUser(anyInt(), anyInt());
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
     }
 
     private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion,
@@ -283,6 +296,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();
@@ -739,8 +764,8 @@
                 MOCK_APPID1);
     }
 
-    @Test
-    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+    private void doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
+            throws Exception {
         doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
@@ -756,8 +781,8 @@
         final Set<UidRange> vpnRange2 = Set.of(new UidRange(MOCK_UID12, MOCK_UID12));
 
         // When VPN is connected, expect a rule to be set up for user app MOCK_UID11
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
 
         reset(mBpfNetMaps);
 
@@ -765,42 +790,54 @@
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11}));
         mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_UID11);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
 
         reset(mBpfNetMaps);
 
         // During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the
         // old UID rules then adds the new ones. Expect netd to be updated
-        mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11}));
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12}));
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID);
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID12}));
 
         reset(mBpfNetMaps);
 
         // When VPN is disconnected, expect rules to be torn down
-        mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12}));
-        assertNull(mPermissionMonitor.getVpnUidRanges("tun0"));
+        assertNull(mPermissionMonitor.getVpnInterfaceUidRanges(ifName));
     }
 
     @Test
-    public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+        doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
+    }
+
+    @Test
+    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
+            throws Exception {
+        doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
+    }
+
+    private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
+            Exception {
         doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 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),
                 UidRange.createForUser(MOCK_USER2));
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
 
         // Newly-installed package should have uid rules added
         addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID21}));
 
         // Removed package should have its uid rules removed
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
@@ -808,6 +845,168 @@
         verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID21}));
     }
 
+    @Test
+    public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+        doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
+    }
+
+    @Test
+    public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
+        doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisable() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // Every app on user 0 except MOCK_UID12 are under VPN.
+        final UidRange[] vpnRange1 = {
+                new UidRange(0, MOCK_UID12 - 1),
+                new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
+        };
+
+        // Add Lockdown uid range, expect a rule to be set up for user app MOCK_UID11
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange1);
+        verify(mBpfNetMaps)
+                .setUidRule(
+                        eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_DENY));
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange1));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range, expect rules to be torn down
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange1);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_ALLOW));
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // MOCK_UID11 is under VPN.
+        final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+        final UidRange[] vpnRange = {range};
+
+        // Add Lockdown uid range at 1st time, expect a rule to be set up
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_DENY));
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
+        // already has the rule
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
+        // the range 2 times.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
+        // twice and we removed twice.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_ALLOW));
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // MOCK_UID11 is under VPN.
+        final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+        final UidRange[] vpnRangeDuplicates = {range, range};
+        final UidRange[] vpnRange = {range};
+
+        // Add Lockdown uid ranges which contains duplicated uid ranges
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRangeDuplicates);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_DENY));
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
+        // ranges we added contains duplicated uid ranges.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_ALLOW));
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithInstallAndUnInstall() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
+
+        mPermissionMonitor.startMonitoring();
+        final UidRange[] vpnRange = {
+                UidRange.createForUser(MOCK_USER1),
+                UidRange.createForUser(MOCK_USER2)
+        };
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+
+        // Installing package should add Lockdown rules
+        addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_DENY));
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
+                        eq(FIREWALL_RULE_DENY));
+
+        reset(mBpfNetMaps);
+
+        // Uninstalling package should remove Lockdown rules
+        mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_ALLOW));
+        verify(mBpfNetMaps, never())
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
+                        eq(FIREWALL_RULE_ALLOW));
+    }
 
     // Normal package add/remove operations will trigger multiple intent for uids corresponding to
     // each user. To simulate generic package operations, the onPackageAdded/Removed will need to be
@@ -881,7 +1080,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);
@@ -1249,4 +1448,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/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 33c0868..eb35469 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -27,9 +27,12 @@
 import static android.net.ConnectivityManager.NetworkCallback;
 import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
+import static android.net.VpnManager.TYPE_VPN_PLATFORM;
+import static android.os.Build.VERSION_CODES.S_V2;
 import static android.os.UserHandle.PER_USER_RANGE;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -42,6 +45,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -54,6 +58,7 @@
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -65,6 +70,7 @@
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -83,20 +89,30 @@
 import android.net.LinkProperties;
 import android.net.LocalSocket;
 import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkProvider;
 import android.net.RouteInfo;
 import android.net.UidRangeParcel;
 import android.net.VpnManager;
+import android.net.VpnProfileState;
 import android.net.VpnService;
 import android.net.VpnTransportInfo;
 import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.exceptions.IkeException;
+import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
+import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
+import android.net.ipsec.ike.exceptions.IkeTimeoutException;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.INetworkManagementService;
+import android.os.Looper;
 import android.os.ParcelFileDescriptor;
+import android.os.PowerWhitelistManager;
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -113,12 +129,17 @@
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
+import com.android.internal.util.HexDump;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.server.DeviceIdleInternal;
 import com.android.server.IpSecService;
+import com.android.server.vcn.util.PersistableBundleUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
@@ -135,6 +156,7 @@
 import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -154,10 +176,13 @@
  */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
+@IgnoreUpTo(VERSION_CODES.S_V2)
 public class VpnTest {
     private static final String TAG = "VpnTest";
 
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     // Mock users
     static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY);
     static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN);
@@ -181,14 +206,16 @@
     private static final String TEST_IFACE_NAME = "TEST_IFACE";
     private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
     private static final long TEST_TIMEOUT_MS = 500L;
-
+    private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
+            "VPN_APP_EXCLUDED_27_com.testvpn.vpn";
     /**
      * Names and UIDs for some fake packages. Important points:
      *  - UID is ordered increasing.
      *  - One pair of packages have consecutive UIDs.
      */
     static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
-    static final int[] PKG_UIDS = {66, 77, 78, 400};
+    static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
+    static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
 
     // Mock packages
     static final Map<String, Integer> mPackages = new ArrayMap<>();
@@ -211,6 +238,7 @@
     @Mock private ConnectivityManager mConnectivityManager;
     @Mock private IpSecService mIpSecService;
     @Mock private VpnProfileStore mVpnProfileStore;
+    @Mock DeviceIdleInternal mDeviceIdleInternal;
     private final VpnProfile mVpnProfile;
 
     private IpSecManager mIpSecManager;
@@ -271,6 +299,11 @@
         doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
     }
 
+    @After
+    public void tearDown() throws Exception {
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+    }
+
     private <T> void mockService(Class<T> clazz, String name, T service) {
         doReturn(service).when(mContext).getSystemService(name);
         doReturn(name).when(mContext).getSystemServiceName(clazz);
@@ -296,6 +329,17 @@
         return new Range<Integer>(start, stop);
     }
 
+    private static String getPackageByteString(List<String> packages) {
+        try {
+            return HexDump.toHexString(
+                    PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList(
+                            packages, PersistableBundleUtils.STRING_SERIALIZER)),
+                        true /* upperCase */);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
     @Test
     public void testRestrictedProfilesAreAddedToVpn() {
         setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
@@ -345,7 +389,11 @@
                 Arrays.asList(packages), null /* disallowedApplications */);
         assertEquals(rangeSet(
                 uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
-                uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2])),
+                uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0])),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))),
                 allow);
 
         // Denied list
@@ -356,10 +404,20 @@
                 uidRange(userStart, userStart + PKG_UIDS[0] - 1),
                 uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
                 /* Empty range between UIDS[1] and UIDS[2], should be excluded, */
-                uidRange(userStart + PKG_UIDS[2] + 1, userStop)),
+                uidRange(userStart + PKG_UIDS[2] + 1,
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)),
                 disallow);
     }
 
+    private void verifyPowerSaveTempWhitelistApp(String packageName) {
+        verify(mDeviceIdleInternal).addPowerSaveTempWhitelistApp(anyInt(), eq(packageName),
+                anyLong(), anyInt(), eq(false), eq(PowerWhitelistManager.REASON_VPN),
+                eq("VpnManager event"));
+    }
+
     @Test
     public void testGetAlwaysAndOnGetLockDown() throws Exception {
         final Vpn vpn = createVpn(primaryUser.id);
@@ -397,18 +455,24 @@
         assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
 
         // Switch to another app.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
     }
 
@@ -423,17 +487,25 @@
                 PKGS[1], true, Collections.singletonList(PKGS[2])));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[]  {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
         // Change allowed app list to PKGS[3].
         assertTrue(vpn.setAlwaysOnPackage(
                 PKGS[1], true, Collections.singletonList(PKGS[3])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
 
         // Change the VPN app.
@@ -441,32 +513,52 @@
                 PKGS[0], true, Collections.singletonList(PKGS[3])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1)
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
         }));
 
         // Remove the list of allowed packages.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop),
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
         }));
 
         // Add the list of allowed packages.
         assertTrue(vpn.setAlwaysOnPackage(
                 PKGS[0], true, Collections.singletonList(PKGS[1])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
 
         // Try allowing a package with a comma, should be rejected.
@@ -479,11 +571,19 @@
                 PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app")));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
     }
 
@@ -528,7 +628,10 @@
         };
         final UidRangeParcel[] exceptPkg0 = {
             new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
-            new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1, entireUser[0].stop)
+            new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1,
+                               Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)),
+            new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1),
+                               entireUser[0].stop),
         };
 
         final InOrder order = inOrder(mConnectivityManager);
@@ -696,6 +799,47 @@
         }
     }
 
+    private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+        when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
+                .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
+
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+        vpn.mNetworkAgent = new NetworkAgent(mContext, Looper.getMainLooper(), TAG,
+                new NetworkCapabilities.Builder().build(), new LinkProperties(), 10 /* score */,
+                new NetworkAgentConfig.Builder().build(),
+                new NetworkProvider(mContext, Looper.getMainLooper(), TAG)) {};
+        return vpn;
+    }
+
+    @Test @IgnoreUpTo(S_V2)
+    public void testSetAndGetAppExclusionList() throws Exception {
+        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+        verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
+        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+        verify(mVpnProfileStore)
+                .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
+                     eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
+        assertEquals(vpn.createUserAndRestrictedProfilesRanges(
+                primaryUser.id, null, Arrays.asList(PKGS)),
+                vpn.mNetworkCapabilities.getUids());
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+    }
+
+    @Test @IgnoreUpTo(S_V2)
+    public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
+        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+        // Mock it to restricted profile
+        when(mUserManager.getUserInfo(anyInt())).thenReturn(restrictedProfileA);
+        // Restricted users cannot configure VPNs
+        assertThrows(SecurityException.class,
+                () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
+        assertThrows(SecurityException.class, () -> vpn.getAppExclusionList(TEST_VPN_PKG));
+    }
+
     @Test
     public void testProvisionVpnProfilePreconsented() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
@@ -783,6 +927,30 @@
         verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
     }
 
+    private void verifyPlatformVpnIsActivated(String packageName) {
+        verify(mAppOps).noteOpNoThrow(
+                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
+                eq(Process.myUid()),
+                eq(packageName),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+        verify(mAppOps).startOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+                eq(Process.myUid()),
+                eq(packageName),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+    }
+
+    private void verifyPlatformVpnIsDeactivated(String packageName) {
+        // Add a small delay to double confirm that finishOp is only called once.
+        verify(mAppOps, after(100)).finishOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+                eq(Process.myUid()),
+                eq(packageName),
+                eq(null) /* attributionTag */);
+    }
+
     @Test
     public void testStartVpnProfile() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
@@ -793,13 +961,7 @@
         vpn.startVpnProfile(TEST_VPN_PKG);
 
         verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        verify(mAppOps)
-                .noteOpNoThrow(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(Process.myUid()),
-                        eq(TEST_VPN_PKG),
-                        eq(null) /* attributionTag */,
-                        eq(null) /* message */);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
     }
 
     @Test
@@ -811,7 +973,7 @@
 
         vpn.startVpnProfile(TEST_VPN_PKG);
 
-        // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown.
+        // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown.
         verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
                 TEST_VPN_PKG, null /* attributionTag */, null /* message */);
     }
@@ -896,18 +1058,7 @@
         when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
                 .thenReturn(mVpnProfile.encode());
         vpn.startVpnProfile(TEST_VPN_PKG);
-        verify(mAppOps).noteOpNoThrow(
-                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        verify(mAppOps).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
         // Add a small delay to make sure that startOp is only called once.
         verify(mAppOps, after(100).times(1)).startOp(
                 eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
@@ -923,12 +1074,7 @@
                 eq(null) /* attributionTag */,
                 eq(null) /* message */);
         vpn.stopVpnProfile(TEST_VPN_PKG);
-        // Add a small delay to double confirm that startOp is only called once.
-        verify(mAppOps, after(100)).finishOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */);
+        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
     }
 
     @Test
@@ -964,6 +1110,144 @@
                 eq(null) /* message */);
     }
 
+    private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
+            int errorCode, VpnProfileState... profileState) {
+        final Context userContext =
+                mContext.createContextAsUser(UserHandle.of(primaryUser.id), 0 /* flags */);
+        final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        final int verifyTimes = (profileState == null) ? 1 : profileState.length;
+        verify(userContext, times(verifyTimes)).startService(intentArgumentCaptor.capture());
+
+        for (int i = 0; i < verifyTimes; i++) {
+            final Intent intent = intentArgumentCaptor.getAllValues().get(i);
+            assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY));
+            final Set<String> categories = intent.getCategories();
+            assertTrue(categories.contains(category));
+            assertEquals(errorClass,
+                    intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */));
+            assertEquals(errorCode,
+                    intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */));
+            if (profileState != null) {
+                assertEquals(profileState[i], intent.getParcelableExtra(
+                        VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class));
+            }
+        }
+        reset(userContext);
+    }
+
+    @Test
+    public void testVpnManagerEventForUserDeactivated() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastT());
+        // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either
+        // null or the package of the caller. This test will call Vpn#prepare() to pretend the old
+        // VPN is replaced by a new one. But only Settings can change to some other packages, and
+        // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the
+        // security checks.
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        // Test the case that the user deactivates the vpn in vpn app.
+        final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+        vpn.stopVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
+        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+        reset(mDeviceIdleInternal);
+        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
+        // errorCode won't be set.
+        verifyVpnManagerEvent(sessionKey1, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                -1 /* errorClass */, -1 /* errorCode */, null /* profileState */);
+        reset(mAppOps);
+
+        // Test the case that the user chooses another vpn and the original one is replaced.
+        final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+        vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM);
+        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
+        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+        reset(mDeviceIdleInternal);
+        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
+        // errorCode won't be set.
+        verifyVpnManagerEvent(sessionKey2, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                -1 /* errorClass */, -1 /* errorCode */, null /* profileState */);
+    }
+
+    @Test
+    public void testVpnManagerEventForAlwaysOnChanged() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastT());
+        // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+        final Vpn vpn = createVpn(primaryUser.id);
+        // Enable VPN always-on for PKGS[1].
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+        // Enable VPN lockdown for PKGS[1].
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, true /* lockdown */));
+
+        // Disable VPN lockdown for PKGS[1].
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+        // Disable VPN always-on.
+        assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, false /* alwaysOn */, false /* lockdown */));
+
+        // Enable VPN always-on for PKGS[1] again.
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+        // Enable VPN always-on for PKGS[2].
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[2]);
+        reset(mDeviceIdleInternal);
+        // PKGS[1] is replaced with PKGS[2].
+        // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to
+        // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to
+        // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled.
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, false /* alwaysOn */, false /* lockdown */),
+                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+    }
+
     @Test
     public void testSetPackageAuthorizationVpnService() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();
@@ -981,7 +1265,7 @@
     public void testSetPackageAuthorizationPlatformVpn() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();
 
-        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM));
+        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM));
         verify(mAppOps)
                 .setMode(
                         eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
@@ -1031,15 +1315,16 @@
                 config -> Arrays.asList(config.flags).contains(flag)));
     }
 
-    @Test
-    public void testStartPlatformVpnAuthenticationFailed() throws Exception {
+    private void setupPlatformVpnWithSpecificExceptionAndItsErrorCode(IkeException exception,
+            String category, int errorType, int errorCode) throws Exception {
         final ArgumentCaptor<IkeSessionCallback> captor =
                 ArgumentCaptor.forClass(IkeSessionCallback.class);
-        final IkeProtocolException exception = mock(IkeProtocolException.class);
-        when(exception.getErrorType())
-                .thenReturn(IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED);
 
-        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), (mVpnProfile));
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
         final NetworkCallback cb = triggerOnAvailableAndGetCallback();
 
         verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1049,10 +1334,77 @@
         verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
                 .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
         final IkeSessionCallback ikeCb = captor.getValue();
-        ikeCb.onClosedExceptionally(exception);
+        ikeCb.onClosedWithException(exception);
 
-        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
-        assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
+        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(sessionKey, category, errorType, errorCode, null /* profileState */);
+        if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
+            verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
+                    .unregisterNetworkCallback(eq(cb));
+        }
+    }
+
+    @Test
+    public void testStartPlatformVpnAuthenticationFailed() throws Exception {
+        final IkeProtocolException exception = mock(IkeProtocolException.class);
+        final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
+        when(exception.getErrorType()).thenReturn(errorCode);
+        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+                VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
+                errorCode);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithRecoverableError() throws Exception {
+        final IkeProtocolException exception = mock(IkeProtocolException.class);
+        final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
+        when(exception.getErrorType()).thenReturn(errorCode);
+        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+                VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception {
+        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+        final UnknownHostException unknownHostException = new UnknownHostException();
+        final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
+        when(exception.getCause()).thenReturn(unknownHostException);
+        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+                errorCode);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception {
+        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+        final IkeTimeoutException ikeTimeoutException =
+                new IkeTimeoutException("IkeTimeoutException");
+        final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
+        when(exception.getCause()).thenReturn(ikeTimeoutException);
+        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+                errorCode);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
+        final IkeNetworkLostException exception = new IkeNetworkLostException(
+                new Network(100));
+        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+                VpnManager.ERROR_CODE_NETWORK_LOST);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithIOException() throws Exception {
+        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+        final IOException ioException = new IOException();
+        final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
+        when(exception.getCause()).thenReturn(ioException);
+        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+                errorCode);
     }
 
     @Test
@@ -1208,7 +1560,7 @@
         }
     }
 
-    private static final class TestDeps extends Vpn.Dependencies {
+    private final class TestDeps extends Vpn.Dependencies {
         public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
         public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
         public final File mStateFile;
@@ -1337,6 +1689,11 @@
 
         @Override
         public void setBlocking(FileDescriptor fd, boolean blocking) {}
+
+        @Override
+        public DeviceIdleInternal getDeviceIdleInternal() {
+            return mDeviceIdleInternal;
+        }
     }
 
     /**
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java
new file mode 100644
index 0000000..a9f80ea
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.ethernet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+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.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class EthernetConfigStoreTest {
+    private static final LinkAddress LINKADDR = new LinkAddress("192.168.1.100/25");
+    private static final InetAddress GATEWAY = InetAddresses.parseNumericAddress("192.168.1.1");
+    private static final InetAddress DNS1 = InetAddresses.parseNumericAddress("8.8.8.8");
+    private static final InetAddress DNS2 = InetAddresses.parseNumericAddress("8.8.4.4");
+    private static final StaticIpConfiguration STATIC_IP_CONFIG =
+            new StaticIpConfiguration.Builder()
+                    .setIpAddress(LINKADDR)
+                    .setGateway(GATEWAY)
+                    .setDnsServers(new ArrayList<InetAddress>(
+                            List.of(DNS1, DNS2)))
+                    .build();
+    private static final ProxyInfo PROXY_INFO = ProxyInfo.buildDirectProxy("test", 8888);
+    private static final IpConfiguration APEX_IP_CONFIG =
+            new IpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null);
+    private static final IpConfiguration LEGACY_IP_CONFIG =
+            new IpConfiguration(IpAssignment.STATIC, ProxySettings.STATIC, STATIC_IP_CONFIG,
+                    PROXY_INFO);
+
+    private EthernetConfigStore mEthernetConfigStore;
+    private File mApexTestDir;
+    private File mLegacyTestDir;
+    private File mApexConfigFile;
+    private File mLegacyConfigFile;
+
+    private void createTestDir() {
+        final Context context = InstrumentationRegistry.getContext();
+        final File baseDir = context.getFilesDir();
+        mApexTestDir = new File(baseDir.getPath() + "/apex");
+        mApexTestDir.mkdirs();
+
+        mLegacyTestDir = new File(baseDir.getPath() + "/legacy");
+        mLegacyTestDir.mkdirs();
+    }
+
+    @Before
+    public void setUp() {
+        createTestDir();
+        mEthernetConfigStore = new EthernetConfigStore();
+    }
+
+    @After
+    public void tearDown() {
+        mApexTestDir.delete();
+        mLegacyTestDir.delete();
+    }
+
+    private void assertConfigFileExist(final String filepath) {
+        assertTrue(new File(filepath).exists());
+    }
+
+    /** Wait for the delayed write operation completes. */
+    private void waitForMs(long ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (final InterruptedException e) {
+            fail("Thread was interrupted");
+        }
+    }
+
+    @Test
+    public void testWriteIpConfigToApexFilePathAndRead() throws Exception {
+        // Write the config file to the apex file path, pretend the config file exits and
+        // check if IP config should be read from apex file path.
+        mApexConfigFile = new File(mApexTestDir.getPath(), "test.txt");
+        mEthernetConfigStore.write("eth0", APEX_IP_CONFIG, mApexConfigFile.getPath());
+        waitForMs(50);
+
+        mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt");
+        final ArrayMap<String, IpConfiguration> ipConfigurations =
+                mEthernetConfigStore.getIpConfigurations();
+        assertEquals(APEX_IP_CONFIG, ipConfigurations.get("eth0"));
+
+        mApexConfigFile.delete();
+    }
+
+    @Test
+    public void testWriteIpConfigToLegacyFilePathAndRead() throws Exception {
+        // Write the config file to the legacy file path, pretend the config file exits and
+        // check if IP config should be read from legacy file path.
+        mLegacyConfigFile = new File(mLegacyTestDir, "test.txt");
+        mEthernetConfigStore.write("0", LEGACY_IP_CONFIG, mLegacyConfigFile.getPath());
+        waitForMs(50);
+
+        mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt");
+        final ArrayMap<String, IpConfiguration> ipConfigurations =
+                mEthernetConfigStore.getIpConfigurations();
+        assertEquals(LEGACY_IP_CONFIG, ipConfigurations.get("0"));
+
+        // Check the same config file in apex file path is created.
+        assertConfigFileExist(mApexTestDir.getPath() + "/test.txt");
+
+        final File apexConfigFile = new File(mApexTestDir.getPath() + "/test.txt");
+        apexConfigFile.delete();
+        mLegacyConfigFile.delete();
+    }
+}
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..4f849d2
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -0,0 +1,717 @@
+/*
+ * 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 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.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.NetworkProvider.NetworkOfferCallback;
+import android.net.NetworkRequest;
+import android.net.StaticIpConfiguration;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.net.module.util.InterfaceParams;
+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 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;
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+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;
+    @Mock private NetworkProvider mNetworkProvider;
+
+    @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, mNetworkProvider, 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));
+
+        ArgumentCaptor<NetworkOfferCallback> captor = ArgumentCaptor.forClass(
+                NetworkOfferCallback.class);
+        verify(mNetworkProvider).registerNetworkOffer(any(), any(), any(), captor.capture());
+        captor.getValue().onNetworkNeeded(createDefaultRequest());
+
+        verifyStart(ipConfig);
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkProvider);
+    }
+
+    // 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.
+        mNetFactory.addInterface(iface, HW_ADDR, createDefaultIpConfig(),
+                createInterfaceCapsBuilder(NetworkCapabilities.TRANSPORT_ETHERNET).build());
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+    }
+
+    @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 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 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);
+    }
+
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.removeInterface(TEST_IFACE));
+    }
+
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
+    }
+
+    @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..b2b9f2c
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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.mock;
+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.EthernetNetworkUpdateRequest;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.os.Build;
+import android.os.Handler;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+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;
+    private Context mContext;
+    private Handler mHandler;
+    private EthernetTracker mEthernetTracker;
+    private PackageManager mPackageManager;
+
+    @Before
+    public void setup() {
+        mContext = mock(Context.class);
+        mHandler = mock(Handler.class);
+        mEthernetTracker = mock(EthernetTracker.class);
+        mPackageManager = mock(PackageManager.class);
+        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 testEnableInterfaceRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.enableInterface("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testDisableInterfaceRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.disableInterface("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(null, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testEnableInterfaceRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.enableInterface(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisableInterfaceRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.disableInterface(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());
+    }
+
+    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 testEnableInterfaceRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisableInterfaceRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disableInterface(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 testEnableInterfaceRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisableInterfaceRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disableInterface(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 testEnableInterface() {
+        mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisableInterface() {
+        mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disableInterface(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 testEnableInterfaceForTestRequestDoesNotRequireNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisableInterfaceForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disableInterface(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..38094ae
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -0,0 +1,518 @@
+/*
+ * 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.doAnswer;
+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.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.EthernetManager;
+import android.net.IEthernetServiceListener;
+import android.net.INetd;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.InetAddresses;
+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.Build;
+import android.os.HandlerThread;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+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.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+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 testEnableInterfaceCorrectlyCallsFactory() {
+        tracker.enableInterface(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisableInterfaceCorrectlyCallsFactory() {
+        tracker.disableInterface(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) { }
+    }
+
+    private InterfaceConfigurationParcel createMockedIfaceParcel(final String ifname,
+            final String hwAddr) {
+        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
+        ifaceParcel.ifName = ifname;
+        ifaceParcel.hwAddr = hwAddr;
+        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
+        return ifaceParcel;
+    }
+
+    @Test
+    public void testListenEthernetStateChange() throws Exception {
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final String testIface = "testtap123";
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel ifaceParcel = createMockedIfaceParcel(testIface,
+                testHwAddr);
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
+        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+
+        final AtomicBoolean ifaceUp = new AtomicBoolean(true);
+        doAnswer(inv -> ifaceUp.get()).when(mFactory).hasInterface(testIface);
+        doAnswer(inv ->
+                ifaceUp.get() ? EthernetManager.STATE_LINK_UP : EthernetManager.STATE_ABSENT)
+                .when(mFactory).getInterfaceState(testIface);
+        doAnswer(inv -> {
+            ifaceUp.set(true);
+            return null;
+        }).when(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
+        doAnswer(inv -> {
+            ifaceUp.set(false);
+            return null;
+        }).when(mFactory).removeInterface(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);
+
+        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);
+
+        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());
+    }
+
+    @Test
+    public void testListenEthernetStateChange_unsolicitedEventListener() throws Exception {
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {});
+        doReturn(new String[] {}).when(mFactory).getAvailableInterfaces(anyBoolean());
+
+        tracker.setIncludeTestInterfaces(true);
+        tracker.start();
+
+        final ArgumentCaptor<EthernetTracker.InterfaceObserver> captor =
+                ArgumentCaptor.forClass(EthernetTracker.InterfaceObserver.class);
+        verify(mNetd, timeout(TIMEOUT_MS)).registerUnsolicitedEventListener(captor.capture());
+        final EthernetTracker.InterfaceObserver observer = captor.getValue();
+
+        tracker.setEthernetEnabled(false);
+        waitForIdle();
+        reset(mFactory);
+        reset(mNetd);
+
+        final String testIface = "testtap1";
+        observer.onInterfaceAdded(testIface);
+        verify(mFactory, never()).addInterface(eq(testIface), anyString(), any(), any());
+        observer.onInterfaceRemoved(testIface);
+        verify(mFactory, never()).removeInterface(eq(testIface));
+
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel testIfaceParce =
+                createMockedIfaceParcel(testIface, testHwAddr);
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(testIfaceParce);
+        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+        tracker.setEthernetEnabled(true);
+        waitForIdle();
+        reset(mFactory);
+
+        final String testIface2 = "testtap2";
+        observer.onInterfaceRemoved(testIface2);
+        verify(mFactory, timeout(TIMEOUT_MS)).removeInterface(eq(testIface2));
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
index 987b7b7..c6852d1 100644
--- a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
@@ -24,16 +24,18 @@
 import android.content.Context;
 import android.net.INetd;
 import android.net.MacAddress;
+import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.Struct.U32;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -42,8 +44,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
 @SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public final class BpfInterfaceMapUpdaterTest {
     private static final int TEST_INDEX = 1;
     private static final int TEST_INDEX2 = 2;
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..4adc999
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.content.Context;
+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.os.Build;
+import android.os.HandlerThread;
+import android.util.ArrayMap;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link IpConfigStore}
+ */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+public class IpConfigStoreTest {
+    private static final int TIMEOUT_MS = 2_000;
+    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";
+
+    private static final ArrayList<InetAddress> DNS_SERVERS = new ArrayList<>(List.of(
+            InetAddresses.parseNumericAddress(DNS_IP_ADDR_1),
+            InetAddresses.parseNumericAddress(DNS_IP_ADDR_2)));
+    private static final StaticIpConfiguration STATIC_IP_CONFIG_1 =
+            new StaticIpConfiguration.Builder()
+                    .setIpAddress(new LinkAddress(IP_ADDR_1))
+                    .setDnsServers(DNS_SERVERS)
+                    .build();
+    private static final StaticIpConfiguration STATIC_IP_CONFIG_2 =
+            new StaticIpConfiguration.Builder()
+                    .setIpAddress(new LinkAddress(IP_ADDR_2))
+                    .setDnsServers(DNS_SERVERS)
+                    .build();
+    private static final ProxyInfo PROXY_INFO =
+            ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2"));
+
+    @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 IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, STATIC_IP_CONFIG_1, PROXY_INFO);
+        final IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, STATIC_IP_CONFIG_2, PROXY_INFO);
+
+        final ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+        expectedNetworks.put(IFACE_1, expectedConfig1);
+        expectedNetworks.put(IFACE_2, expectedConfig2);
+
+        final MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
+        final IpConfigStore store = new IpConfigStore(writer);
+        store.writeIpConfigurations("file/path/not/used/", expectedNetworks);
+
+        final InputStream in = new ByteArrayInputStream(writer.mByteStream.toByteArray());
+        final 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));
+    }
+
+    @Test
+    public void readIpConfigurationFromFilePath() throws Exception {
+        final HandlerThread testHandlerThread = new HandlerThread("IpConfigStoreTest");
+        final DelayedDiskWrite.Dependencies dependencies =
+                new DelayedDiskWrite.Dependencies() {
+                    @Override
+                    public HandlerThread makeHandlerThread() {
+                        return testHandlerThread;
+                    }
+                    @Override
+                    public void quitHandlerThread(HandlerThread handlerThread) {
+                        testHandlerThread.quitSafely();
+                    }
+        };
+
+        final IpConfiguration ipConfig = newIpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, STATIC_IP_CONFIG_1, PROXY_INFO);
+        final ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+        expectedNetworks.put(IFACE_1, ipConfig);
+
+        // Write IP config to specific file path and read it later.
+        final Context context = InstrumentationRegistry.getContext();
+        final File configFile = new File(context.getFilesDir().getPath(),
+                "IpConfigStoreTest-ipconfig.txt");
+        final DelayedDiskWrite writer = new DelayedDiskWrite(dependencies);
+        final IpConfigStore store = new IpConfigStore(writer);
+        store.writeIpConfigurations(configFile.getPath(), expectedNetworks);
+        HandlerUtils.waitForIdle(testHandlerThread, TIMEOUT_MS);
+
+        // Read IP config from the file path.
+        final ArrayMap<String, IpConfiguration> actualNetworks =
+                IpConfigStore.readIpConfigurations(configFile.getPath());
+        assertNotNull(actualNetworks);
+        assertEquals(1, actualNetworks.size());
+        assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
+
+        // Return an empty array when reading IP configuration from an non-exist config file.
+        final ArrayMap<String, IpConfiguration> emptyNetworks =
+                IpConfigStore.readIpConfigurations("/dev/null");
+        assertNotNull(emptyNetworks);
+        assertEquals(0, emptyNetworks.size());
+
+        configFile.delete();
+    }
+
+    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 79744b1..5400a00 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -29,6 +29,7 @@
 import static android.net.NetworkStats.UID_ALL;
 
 import static com.android.server.net.NetworkStatsFactory.kernelToTag;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
@@ -38,7 +39,6 @@
 import android.net.NetworkStats;
 import android.net.TrafficStats;
 import android.net.UnderlyingNetworkInfo;
-import android.os.Build;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -67,7 +67,7 @@
 /** Tests for {@link NetworkStatsFactory}. */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class NetworkStatsFactoryTest extends NetworkStatsBaseTest {
     private static final String CLAT_PREFIX = "v4-";
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 5f9d1ff..5747e10 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -32,6 +32,7 @@
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
@@ -64,6 +65,7 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.Objects;
 
 /**
@@ -86,11 +88,19 @@
     private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
     private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
 
+    private static final int PID_SYSTEM = 1234;
+    private static final int PID_RED = 1235;
+    private static final int PID_BLUE = 1236;
+
     private static final int UID_RED = UserHandle.PER_USER_RANGE + 1;
     private static final int UID_BLUE = UserHandle.PER_USER_RANGE + 2;
     private static final int UID_GREEN = UserHandle.PER_USER_RANGE + 3;
     private static final int UID_ANOTHER_USER = 2 * UserHandle.PER_USER_RANGE + 4;
 
+    private static final String PACKAGE_SYSTEM = "android";
+    private static final String PACKAGE_RED = "RED";
+    private static final String PACKAGE_BLUE = "BLUE";
+
     private static final long WAIT_TIMEOUT_MS = 500;
     private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES;
     private static final long BASE_BYTES = 7 * MB_IN_BYTES;
@@ -131,14 +141,15 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
 
         final DataUsageRequest requestByApp = mStatsObservers.register(mContext, inputRequest,
-                mUsageCallback, UID_RED, NetworkStatsAccess.Level.DEVICE);
+                mUsageCallback, PID_RED , UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(requestByApp.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, requestByApp.template));
         assertEquals(thresholdTooLowBytes, requestByApp.thresholdInBytes);
 
         // Verify the threshold requested by system uid won't be overridden.
         final DataUsageRequest requestBySystem = mStatsObservers.register(mContext, inputRequest,
-                mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                mUsageCallback, PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM,
+                NetworkStatsAccess.Level.DEVICE);
         assertTrue(requestBySystem.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template));
         assertEquals(1, requestBySystem.thresholdInBytes);
@@ -151,7 +162,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
         assertEquals(highThresholdBytes, request.thresholdInBytes);
@@ -163,19 +174,64 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
 
         DataUsageRequest request1 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request1.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request1.template));
         assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
 
         DataUsageRequest request2 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request2.requestId > request1.requestId);
         assertTrue(Objects.equals(sTemplateWifi, request2.template));
         assertEquals(THRESHOLD_BYTES, request2.thresholdInBytes);
     }
 
     @Test
+    public void testRegister_limit() throws Exception {
+        final DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
+
+        // Register maximum requests for red.
+        final ArrayList<DataUsageRequest> redRequests = new ArrayList<>();
+        for (int i = 0; i < NetworkStatsObservers.MAX_REQUESTS_PER_UID; i++) {
+            final DataUsageRequest returnedRequest =
+                    mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                            PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
+            redRequests.add(returnedRequest);
+            assertTrue(returnedRequest.requestId > 0);
+        }
+
+        // Verify request exceeds the limit throws.
+        assertThrows(IllegalStateException.class, () ->
+                mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                    PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE));
+
+        // Verify another uid is not affected.
+        final ArrayList<DataUsageRequest> blueRequests = new ArrayList<>();
+        for (int i = 0; i < NetworkStatsObservers.MAX_REQUESTS_PER_UID; i++) {
+            final DataUsageRequest returnedRequest =
+                    mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                            PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.DEVICE);
+            blueRequests.add(returnedRequest);
+            assertTrue(returnedRequest.requestId > 0);
+        }
+
+        // Again, verify request exceeds the limit throws for the 2nd uid.
+        assertThrows(IllegalStateException.class, () ->
+                mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                        PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE));
+
+        // Unregister all registered requests. Note that exceptions cannot be tested since
+        // unregister is handled in the handler thread.
+        for (final DataUsageRequest request : redRequests) {
+            mStatsObservers.unregister(request, UID_RED);
+        }
+        for (final DataUsageRequest request : blueRequests) {
+            mStatsObservers.unregister(request, UID_BLUE);
+        }
+    }
+
+    @Test
     public void testUnregister_unknownRequest_noop() throws Exception {
         DataUsageRequest unknownRequest = new DataUsageRequest(
                 123456 /* id */, sTemplateWifi, THRESHOLD_BYTES);
@@ -189,7 +245,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -209,7 +265,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.DEVICE);
+                PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -218,8 +274,12 @@
 
         mStatsObservers.unregister(request, UID_BLUE);
         waitForObserverToIdle();
-
         Mockito.verifyZeroInteractions(mUsageCallbackBinder);
+
+        // Verify that system uid can unregister for other uids.
+        mStatsObservers.unregister(request, Process.SYSTEM_UID);
+        waitForObserverToIdle();
+        mUsageCallback.expectOnCallbackReleased(request);
     }
 
     private NetworkIdentitySet makeTestIdentSet() {
@@ -237,7 +297,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -261,7 +321,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -291,7 +351,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -322,7 +382,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.DEFAULT);
+                PID_RED, UID_RED, PACKAGE_SYSTEM , NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -355,7 +415,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
+                PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -387,7 +447,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_BLUE, NetworkStatsAccess.Level.USER);
+                PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -420,7 +480,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.USER);
+                PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index ceeb997..f1820b3 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -18,6 +18,7 @@
 
 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_UID_REMOVED;
 import static android.content.Intent.EXTRA_UID;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -56,6 +57,9 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.net.TrafficStats.UID_REMOVED;
 import static android.net.TrafficStats.UID_TETHERING;
+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.text.format.DateUtils.DAY_IN_MILLIS;
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
@@ -77,6 +81,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
@@ -96,6 +101,7 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkStateSnapshot;
 import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.net.TelephonyNetworkSpecifier;
@@ -104,6 +110,7 @@
 import android.net.UnderlyingNetworkInfo;
 import android.net.netstats.provider.INetworkStatsProviderCallback;
 import android.net.wifi.WifiInfo;
+import android.os.DropBoxManager;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -112,11 +119,13 @@
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
 
 import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.util.FileRotator;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.LocationPermissionChecker;
@@ -131,6 +140,16 @@
 import com.android.testutils.TestBpfMap;
 import com.android.testutils.TestableNetworkStatsProviderBinder;
 
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.ZoneOffset;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
 import libcore.testing.io.TestIoUtils;
 
 import org.junit.After;
@@ -142,13 +161,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.io.File;
-import java.time.Clock;
-import java.time.ZoneOffset;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 /**
  * Tests for {@link NetworkStatsService}.
  *
@@ -187,6 +199,7 @@
     private long mElapsedRealtime;
 
     private File mStatsDir;
+    private File mLegacyStatsDir;
     private MockContext mServiceContext;
     private @Mock TelephonyManager mTelephonyManager;
     private static @Mock WifiInfo sWifiInfo;
@@ -220,6 +233,12 @@
     private ContentObserver mContentObserver;
     private Handler mHandler;
     private TetheringManager.TetheringEventCallback mTetheringEventCallback;
+    private Map<String, NetworkStatsCollection> mPlatformNetworkStatsCollection =
+            new ArrayMap<String, NetworkStatsCollection>();
+    private boolean mStoreFilesInApexData = false;
+    private int mImportLegacyTargetAttempts = 0;
+    private @Mock PersistentInt mImportLegacyAttemptsCounter;
+    private @Mock PersistentInt mImportLegacySuccessesCounter;
 
     private class MockContext extends BroadcastInterceptingContext {
         private final Context mBaseContext;
@@ -286,6 +305,8 @@
                 any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
         when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
         mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+        mLegacyStatsDir = TestIoUtils.createTemporaryDirectory(
+                getClass().getSimpleName() + "-legacy");
 
         PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
                 Context.POWER_SERVICE);
@@ -295,8 +316,7 @@
         mHandlerThread = new HandlerThread("HandlerThread");
         final NetworkStatsService.Dependencies deps = makeDependencies();
         mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
-                mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir,
-                getBaseDir(mStatsDir), deps);
+                mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
 
         mElapsedRealtime = 0L;
 
@@ -339,6 +359,44 @@
     private NetworkStatsService.Dependencies makeDependencies() {
         return new NetworkStatsService.Dependencies() {
             @Override
+            public File getLegacyStatsDir() {
+                return mLegacyStatsDir;
+            }
+
+            @Override
+            public File getOrCreateStatsDir() {
+                return mStatsDir;
+            }
+
+            @Override
+            public boolean getStoreFilesInApexData() {
+                return mStoreFilesInApexData;
+            }
+
+            @Override
+            public int getImportLegacyTargetAttempts() {
+                return mImportLegacyTargetAttempts;
+            }
+
+            @Override
+            public PersistentInt createImportLegacyAttemptsCounter(
+                    @androidx.annotation.NonNull Path path) {
+                return mImportLegacyAttemptsCounter;
+            }
+
+            @Override
+            public PersistentInt createImportLegacySuccessesCounter(
+                    @androidx.annotation.NonNull Path path) {
+                return mImportLegacySuccessesCounter;
+            }
+
+            @Override
+            public NetworkStatsCollection readPlatformCollection(
+                    @NonNull String prefix, long bucketDuration) {
+                return mPlatformNetworkStatsCollection.get(prefix);
+            }
+
+            @Override
             public HandlerThread makeHandlerThread() {
                 return mHandlerThread;
             }
@@ -1704,10 +1762,108 @@
         assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
     }
 
-    private static File getBaseDir(File statsDir) {
-        File baseDir = new File(statsDir, "netstats");
-        baseDir.mkdirs();
-        return baseDir;
+    /**
+     * Verify the service will perform data migration process can be controlled by the device flag.
+     */
+    @Test
+    public void testDataMigration() throws Exception {
+        assertStatsFilesExist(false);
+        expectDefaultSettings();
+
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        // expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+                .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.noteUidForeground(UID_RED, false);
+        verify(mUidCounterSetMap, never()).deleteEntry(any());
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
+        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);
+
+        forcePollAndWaitForIdle();
+        // Simulate shutdown to force persisting data
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(true);
+
+        // Move the files to the legacy directory to simulate an import from old data
+        for (File f : mStatsDir.listFiles()) {
+            Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+        }
+        assertStatsFilesExist(false);
+
+        // Fetch the stats from the legacy files and set platform stats collection to be identical
+        mPlatformNetworkStatsCollection.put(PREFIX_DEV,
+                getLegacyCollection(PREFIX_DEV, false /* includeTags */));
+        mPlatformNetworkStatsCollection.put(PREFIX_XT,
+                getLegacyCollection(PREFIX_XT, false /* includeTags */));
+        mPlatformNetworkStatsCollection.put(PREFIX_UID,
+                getLegacyCollection(PREFIX_UID, false /* includeTags */));
+        mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+                getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+        // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+        assertStatsFilesExist(false);
+
+        // Set the flag and reboot, verify the imported data is not there until next boot.
+        mStoreFilesInApexData = true;
+        mImportLegacyTargetAttempts = 3;
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(false);
+
+        // Boot through systemReady() again.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+
+        // After systemReady(), the service should have historical stats loaded again.
+        // Thus, verify
+        //  1. The stats are absorbed by the recorder.
+        //  2. The imported data are persisted.
+        //  3. The attempts count is set to target attempts count to indicate a successful
+        //     migration.
+        assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
+        assertStatsFilesExist(true);
+        verify(mImportLegacyAttemptsCounter).set(3);
+        verify(mImportLegacySuccessesCounter).set(1);
+
+        // TODO: Verify upgrading with Exception won't damege original data and
+        //  will decrease the retry counter by 1.
+    }
+
+    private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
+            boolean includeTags) {
+        final NetworkStats.NonMonotonicObserver observer =
+                mock(NetworkStats.NonMonotonicObserver.class);
+        final DropBoxManager dropBox = mock(DropBoxManager.class);
+        return new NetworkStatsRecorder(new FileRotator(
+                directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+                observer, dropBox, prefix, config.bucketDuration, includeTags);
+    }
+
+    private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
+        final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, PREFIX_DEV,
+                mSettings.getDevConfig(), includeTags);
+        return recorder.getOrLoadCompleteLocked();
     }
 
     private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
@@ -1816,11 +1972,10 @@
     }
 
     private void assertStatsFilesExist(boolean exist) {
-        final File basePath = new File(mStatsDir, "netstats");
         if (exist) {
-            assertTrue(basePath.list().length > 0);
+            assertTrue(mStatsDir.list().length > 0);
         } else {
-            assertTrue(basePath.list().length == 0);
+            assertTrue(mStatsDir.list().length == 0);
         }
     }
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
index 0d34609..622f2be 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -19,6 +19,8 @@
 import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE;
 import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
@@ -37,7 +39,6 @@
 import android.annotation.Nullable;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
-import android.os.Build;
 import android.os.Looper;
 import android.os.Parcel;
 import android.telephony.SubscriptionManager;
@@ -63,7 +64,7 @@
 import java.util.concurrent.Executors;
 
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public final class NetworkStatsSubscriptionsMonitorTest {
     private static final int TEST_SUBID1 = 3;
     private static final int TEST_SUBID2 = 5;
diff --git a/tests/unit/java/com/android/server/net/PersistentIntTest.kt b/tests/unit/java/com/android/server/net/PersistentIntTest.kt
new file mode 100644
index 0000000..9268352
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/PersistentIntTest.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.util.SystemConfigFileCommitEventLogger
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
+import com.android.testutils.assertThrows
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.attribute.PosixFilePermission
+import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE
+import java.nio.file.attribute.PosixFilePermission.OWNER_READ
+import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE
+import java.util.Random
+import kotlin.test.assertEquals
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(SC_V2)
+class PersistentIntTest {
+    val tempFilesCreated = mutableSetOf<Path>()
+    lateinit var tempDir: Path
+
+    @Before
+    fun setUp() {
+        tempDir = Files.createTempDirectory("tmp.PersistentIntTest.")
+    }
+
+    @After
+    fun tearDown() {
+        var permissions = setOf(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE)
+        Files.setPosixFilePermissions(tempDir, permissions)
+
+        for (file in tempFilesCreated) {
+            Files.deleteIfExists(file)
+        }
+        Files.delete(tempDir)
+    }
+
+    @Test
+    fun testNormalReadWrite() {
+        // New, initialized to 0.
+        val pi = createPersistentInt()
+        assertEquals(0, pi.get())
+        pi.set(12345)
+        assertEquals(12345, pi.get())
+
+        // Existing.
+        val pi2 = createPersistentInt(pathOf(pi))
+        assertEquals(12345, pi2.get())
+    }
+
+    @Test
+    fun testReadOrWriteFailsInCreate() {
+        setWritable(tempDir, false)
+        assertThrows(IOException::class.java) {
+            createPersistentInt()
+        }
+    }
+
+    @Test
+    fun testReadOrWriteFailsAfterCreate() {
+        val pi = createPersistentInt()
+        pi.set(42)
+        assertEquals(42, pi.get())
+
+        val path = pathOf(pi)
+        setReadable(path, false)
+        assertThrows(IOException::class.java) { pi.get() }
+        pi.set(77)
+
+        setReadable(path, true)
+        setWritable(path, false)
+        setWritable(tempDir, false) // Writing creates a new file+renames, make this fail.
+        assertThrows(IOException::class.java) { pi.set(99) }
+        assertEquals(77, pi.get())
+    }
+
+    fun addOrRemovePermission(p: Path, permission: PosixFilePermission, add: Boolean) {
+        val permissions = Files.getPosixFilePermissions(p)
+        if (add) {
+            permissions.add(permission)
+        } else {
+            permissions.remove(permission)
+        }
+        Files.setPosixFilePermissions(p, permissions)
+    }
+
+    fun setReadable(p: Path, readable: Boolean) {
+        addOrRemovePermission(p, OWNER_READ, readable)
+    }
+
+    fun setWritable(p: Path, writable: Boolean) {
+        addOrRemovePermission(p, OWNER_WRITE, writable)
+    }
+
+    fun pathOf(pi: PersistentInt): Path {
+        return File(pi.path).toPath()
+    }
+
+    fun createPersistentInt(path: Path = randomTempPath()): PersistentInt {
+        tempFilesCreated.add(path)
+        return PersistentInt(path.toString(),
+                SystemConfigFileCommitEventLogger("PersistentIntTest"))
+    }
+
+    fun randomTempPath(): Path {
+        return tempDir.resolve(Integer.toHexString(Random().nextInt())).also {
+            tempFilesCreated.add(it)
+        }
+    }
+}